Lightweight ASP.NET Background Processes

Imagine a tool which performs some long running background task on the server (on the order of several minutes) and wants to notify users of completion. This notification may take the form of an email, or it may be that the user remains on the website and receives a visual confirmation that the task is complete. The “industrial strength” solution to this is Signal R, which provides all sorts of client/server connectivity capabilities.

In many cases, this may be an excessive solution. Running a background task, with both backend and frontend notifications, is straightforward and doesn’t require a lot of wiring. This post shows one simple approach that will work with ASP.NET Web API or MVC.

Core Concept

Use a dictionary in application state to track background tasks and their status. Match each task to an ID for reportability.

This technique will not work if the runtime of the background task is too long compared to the application idle time-out on IIS. For this reason, the technique is appropriate only for non-essential tasks which don’t expect to run more than a few minutes.

Long Running Job Class

Initiating, checking, and maintaining jobs is handled by the long running jobs class. The class has static methods to create and check a job; and instance values of an ID (which can be sent to/from the client) and the task itself.

Non-static fields and methods are the easiest, primarily just wrappers around the ID and Task.

public class LongRunningJob
{

  public enum JobStatus { Running, Done, Failed }

  /// <summary>
  /// A unique identifier for this job.  Send this ID to the client, 
  /// and it may use it to query the status of the job.
  /// </summary>
  public readonly Guid ID;

  /// <summary>
  /// The actual background task.  Use a continuation to send email 
  /// or perform other server-side operations when the task completes.
  /// </summary>
  public readonly Task Task;
                
  protected LongRunningJob(Guid id, Task task)
  {
    ID = id;
    Task = task;
  }

  /// <summary>
  /// Returns a status based on the Task
  /// </summary>
  public JobStatus Status
  {
    get
    {
      if (Task.IsFaulted)
        return JobStatus.Failed;
      else if (Task.IsCompleted || Task.IsCanceled)
        return JobStatus.Done;
      else
        return JobStatus.Running;
    }

  }

}

The real work is done in the static methods which provide factory construction of a long running job, and status querying via the application state dictionary. The dictionary is accessed via a “Tasks” static property.

Starting a new job is done via the factory method:

/// <summary>
/// Start a new job.  The method provided will be placed into the task 
/// queue and may be started immediately.
/// </summary>
/// <param name="job">The method to run in the background</param>
/// <returns>An object containing the task and a unique identifier that
/// can be used to retrieve the job (and check its status)</returns>
public static LongRunningJob StartJob(Action job)
{
  Guid id = Guid.NewGuid();
  var task = Task.Factory.StartNew(job);
  var lrj = new LongRunningJob(id, task);
  Tasks.Add(id, lrj);
  return lrj;
}

Retrieving a job by ID queries the Tasks dictionary:

/// <summary>
/// Retrieve a job by ID.  If no matching job is found, 
/// returns null.
/// </summary>
/// <param name="id">The ID from LongRunningJob.ID</param>
/// <returns>The LongRunningJob, 
/// or null if no matching job found</returns>
public static LongRunningJob RetrieveJob(Guid id)
{
  if (Tasks.ContainsKey(id))
  {
    return Tasks[id];
  }
  else
  {
    return null;
  }
}

We define the application state dictionary of tasks via a property which instantiates it on first request.

protected static IDictionary<Guid, LongRunningJob> Tasks
{
  get
  {
    var dict = HttpContext.Current.Application["_LongRunningJob"] as 
      IDictionary<Guid, LongRunningJob>;
    if (dict == null)
    {
      dict = new Dictionary<Guid, LongRunningJob>();
      HttpContext.Current.Application["_LongRunningJob"] = dict;
    }
    return dict;
  }
}

Sample Usage

To demonstrate usage, we create a sample application which performs long-running jobs as sleeping threads of various lengths. Failures can be introduced by intentionally throwing exceptions.

Here is the sample Web API controller methods:

[HttpPost]
public Guid CreateRegularJob([FromBody] int seconds)
{
  var job = Models.LongRunningJob.StartJob(new Action(() => 
  {
    // TODO: some long running task
    System.Threading.Thread.Sleep(seconds * 1000);
  }));
  job.Task.ContinueWith(new Action<System.Threading.Tasks.Task>(t => {
    // TODO: send email notification of completion
  }));
  return job.ID;
}

[HttpPost]
public Guid CreateFailJob([FromBody] int seconds)
{
  var job = Models.LongRunningJob.StartJob(new Action(() =>
  {
    System.Threading.Thread.Sleep(seconds * 1000);
    throw new Exception();
  }));
  return job.ID;
}

[HttpGet]
public Models.LongRunningJob.JobStatus CheckStatus(Guid id)
{
  var job = Models.LongRunningJob.RetrieveJob(id);
  return job.Status;               
}

On the client side, sample jobs are created to POST’ing to the appropriate create jobs methods. Each ID is placed in a table, and a polling routine queries the status of each running ID each second until they complete or fail. The ajax calls return without blocking for the long running task to complete.

A frontend sample shows several jobs queued up to run simultaneously for various lengths:
longjobpoll

Future Work

  • Tasks can be extended to return data.
  • Old jobs should be removed from the dictionary to avoid memory leaks.

Comments are closed.