Execute Long Running ASP.NET Process on a Background Thread

A common challenge ASP.NET developers run into is where you need to kick off a process that takes some time - for example some complicated database operation that required initial user inputs. If the process doesn't take say more than 30 seconds or so, you can show a progress indicator on the page and most users will understand that soon it will finish. But what if the process takes 3 minutes, 10 minutes or longer?

The best solution for this is to run the process on a background thread so that it doesn't interfere with the user being able to do other things in your Web Application.  Here is a relatively simple way to do this:

For this demo I am going to use the  ParameterizedThreadStart method to kick off a background thread with a method that acts as a surrogate for an actual long-running process. Here is the demo class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;

namespace LongProcess
{
    public class MyLongProcessClass
    {

         public void DoLongProcess (object parms)
        {
            object[] stuff = (object[]) parms;
             // delete any previous files
            System.IO.File.Delete(Global.FilePath );
            // the line below is a surrogate for whatever your business logic does that takes 10 seconds.
            Thread.Sleep(10000);
            string path = Global.FilePath;
             // write the ready message to the file
            System.IO.File.WriteAllText(path, "READY " +(string)stuff[0] +"|" + Convert.ToString(stuff[1]));
        }
    }
}

You can see above that the DoLongProcess method accepts a single parameter of type Object (this is required for the ParameterizedThreadStart method) and that in this case that parameter will be an Object[] array containing more than one value. I also use a file to keep track of results in order not to be dependent on Session or Cache, as that context may not be available on a background thread.

In my Global.asax.cs class, I have the following method:

  public static string FilePath;

  public static void ProcessList(object[] parms)
        {
            var pc = new MyLongProcessClass();
            ParameterizedThreadStart ts = new ParameterizedThreadStart(pc.DoLongProcess);
            Thread thd = new Thread(ts);
            thd.IsBackground = true;
            thd.Start(parms);
        }

Because it is public and static, this method can be kicked off by any of the pages in the app via "Global.ProcessList( parms)". However, a new instance of the MyLongProcess class is created on every call to the static method. Note also that the thread we create is set to be a background thread. This method call will not block - it will return almost immediately because it is not being called on the main thread.

In my Default.aspx page I have the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace LongProcess
{
    public partial class _Default : System.Web.UI.Page
    {
         protected void Page_Load(object sender, EventArgs e)
         {
             if(User.Identity.IsAuthenticated==false)
                 Response.Redirect("~/Account/Login.aspx");
            txtUserName.Text = User.Identity.Name;
            Global.FilePath = Server.MapPath("~/App_Data/stuff.txt");
            if (Request.QueryString["msg"] != null)
                 this.lblMessage.Text = Request.QueryString["msg"];
        }

         protected void btnSubmit_Click(object sender, EventArgs e)
        {
            string user = txtUserName.Text;
            int processId = int.Parse(txtProcessId.Text);
            object[] parms = new object[] {user, processId};
            Global.ProcessList(parms);
            Label1.Text = "Process is Running.";
        }

         protected void LinkButton1_Click(object sender, EventArgs e)
         {
             Response.Redirect("Wait.aspx");
        }
    }
}

I'm using a modified version of Mads Kristensen's XmlMembershipProvider to allow for standard ASP.NET log-ins without a database. So we require the user to be logged in. If they are, I set the Global FilePath to a specified file located in the App_Data folder, where it will not cause my ASP.NET app to recycle if the file is changed. Also, since I redirect back to this page when the process is done, I have some code to grab and display the message from the querystring.

The Submit button click handler gets the username and an integer value for a process id from a couple of textboxes, creates the object[] parms array, and calls my Global.ProcessList method. I show the user that the process is now running, and i provide a link that will take them to "Wait.aspx" page. Note that this is just a demo, you'll want to validate your inputs in the real world.

The Wait.aspx page has the following logic:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Threading;

namespace LongProcess
{
    public partial class Wait : System.Web.UI.Page
    {
         protected void Page_Load(object sender, EventArgs e)
        {
          
            string x = null;
            try
            {
                 // see if the "completed" file is there
                  x = System.IO.File.ReadAllText(Global.FilePath);
             }
             catch
             {
                  // Could use File.Exists to avoid an exception you know!
                 // file not there yet.
            }
            if (x == null)
                 {
                 }
                 else
            {
               
                 // Delete the file since we are done with it
                     System.IO.File.Delete(Global.FilePath);
                 // redirect back to the Default.aspx page with our completed message
                 Response.Redirect("Default.aspx?msg="+x);
                  
                }

            }
        }
    }

    The markup simply has some script that makes the page reload every 2 seconds:

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Wait.aspx.cs" Inherits="LongProcess.Wait" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script>
    var done = null;
    function reload() {

        document.location.href = 'Wait.aspx';
    }
  
</script>
    <title></title>
</head>
<body onload="done=setTimeout('reload()', 2000)">
    <form id="form1" runat="server">
    <div>
    <asp:Label ID=lblMessage runat=server Text="Waiting..." />
    </div>
    </form>
</body>
</html>

Of course you do not have to show a Wait page; you could choose to simply inform the user that the process will take a long time and that you'll send them an email with a link as soon as it's done. You would do something like this at the very end of the body of the DoLongProcess method.

I also have an animated gif progress indicator on the Default.aspx page just in case you want to do it that way.

And that is it! You can try this out via the downloadable Visual Studio 2010 solution below, which is 100% self-contained and needs no setup of any kind. The initial username and password to use are "test", "test".

Download the Visual Studio 2010 Solution that accompanies this article.

By Peter Bromberg   Popularity  (8533 Views)