5
\$\begingroup\$

I have 2 alternatives for executing recurring tasks in ASP.NET MVC.

We just need to add some code in Global.asax

First alternative:

private static void ThreadSendNextMail()
{
  while (true)
  {
    HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
    {
      using (var db = new EFDbContext())
      {
        try
        {
          //demo test for periodically tasks
          int paymentCount = await db.Payments.AsNoTracking().CountAsync();
          logger.Info(paymentCount);
        }
        catch (Exception ex)
        {
          logger.Error(ex.ToString());
        }
      }
    });
    Thread.Sleep(20000);
  }
}

private Thread mailThread { get; set; }

protected void Application_Start()
{
  //other code removed for clarity
  mailThread = new Thread(new ThreadStart(ThreadSendNextMail));
  mailThread.Start();
}

Second alternative:

private void AddTask(Action<string, object, CacheItemRemovedReason> target, int seconds)
{
  HttpRuntime.Cache.Insert(target.Method.Name + DateTime.Now.ToString(), seconds, null, DateTime.Now.AddSeconds(seconds), Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, new CacheItemRemovedCallback(target));
}

public void SendNextMail(string key, object seconds, CacheItemRemovedReason reason)
{
  HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
  {
    using (var db = new EFDbContext())
    {
      try
      {
        //demo test for periodically tasks
        int paymentCount = await db.Payments.AsNoTracking().CountAsync();
        logger.Info(paymentCount);
      }
      catch (Exception ex)
      {
        logger.Error(ex.ToString());
      }
    }
  });

  AddTask(SendNextMail, Convert.ToInt32(seconds));
}

protected void Application_Start()
{
  //other code removed for clarity

  //Periodically tasks
  AddTask(SendNextMail, 20);
}

I use .NET 4.5.2 and MVC 5.2. For logging I use NLog.

Both methods work perfectly (You can check this by watch log time in log file). Which is more preferable and better for performance, if I have many recurring tasks in the same time?

Do you see any improvement / issue?

\$\endgroup\$
  • 4
    \$\begingroup\$ The first will block a thread for 20 seconds, never ever do that \$\endgroup\$ – Anders Feb 14 '16 at 11:52
3
\$\begingroup\$

As already mentioned by Anders, the second approach is better, however there are several problems when implementing async Tasks that run within an IIS worker process:

1) unhandled exceptions in the async threads/tasks kill the entire process. This should not be a problem in your code, since you catch all exceptions and normally, NLog handle does not have unhandled exception.

2) worker process may suddenly stop for various reasons such as web.config, binaries, idle recycling and the task might be stopped in the middle of its work. This should not happen in your case, since the operation seems pretty short and ASP.NET will try to delay the shutdown of the process to allow background workers to finish their jobs.

I also remark the possible optimization brought by using an Async function along with await on the same instruction.

A small improvement is to use UnitOfWork pattern for your database context, but the benefit comes when more complex things happen in your worker:

/// <summary>
/// unit of work store
/// </summary>
public static class UnitOfWorkStore
{
    public static object GetData(string key)
    {
        if (HttpContext.Current != null)
            return HttpContext.Current.Items[key];
        return CallContext.GetData(key);
    }

    public static void SetData(string key, object data)
    {
        if (HttpContext.Current != null)
            HttpContext.Current.Items[key] = data;
        else
            CallContext.SetData(key, data);
    }
}

public partial class EFDbContext
{
    #region Members
    private static readonly object objSync = new object();
    private static readonly string DATACONTEXT_KEY = "EFDbContext_UserInstance";
    #endregion

    /// <summary>
    /// Uses a UnitOfWorkStore to return the same instance of Context, that is unique
    /// per user, per postback (or CallContext, if not in an HttpContext environment, such as unit tests or console apps)
    /// </summary>
    public static EFDbContext Instance
    {
        get
        {
            // Dirty (non thread-safe) check
            if (UnitOfWorkStore.GetData(DATACONTEXT_KEY) == null)
            {
                lock (objSync)
                {
                    // Thread-safe check
                    if (UnitOfWorkStore.GetData(DATACONTEXT_KEY) == null)
                    {
                        var context = new EFDbContext();
                        UnitOfWorkStore.SetData(DATACONTEXT_KEY, context);
                    }
                }
            }
            return (EFDbContext)UnitOfWorkStore.GetData(DATACONTEXT_KEY);
        }
    }
}

So, that generating a new context can be done using EFDbContext.Instance and unit of work pattern will take care to return the same instance if called in the same "context". Also, it looks like database context disposal is not really necessary.

[later edit]

Just remembered a couple of things:

3) AsNoTracking() is a good option when fetching read-only data. It is faster and allocates less memory, so its usage is welcomed (however, I rarely see it used).

4) Idle timeout - by default, IIS will stop the application pool if it has not activity. This means that no processing will be made during some periods (e.g. during week-ends)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.