Adding Background Tasks to a Web App using C# and ASP.Net

5/28/2010

I could have sworn that I posted something about this before, but apparently not. Anyway, a while back I was working on a project that required a number of tasks to occur that couldn't be accomplished easily in the normal post/response of a web request. Basically they were database and file updates that took about five to ten minutes to complete and needed to occur in 30 minute intervals. Now normally I would create a separate desktop app that would just run on the server that would handle the task but I didn't have rights to do that... So I had to build this as a set of background tasks within a web app. And I made it generic enough that I could package it up and release it. So let's look at some code:

   1: /// <summary>
   2:  
   3: /// Manager for the task scheduler
   4:  
   5: /// </summary>
   6:  
   7: public class Manager:Singleton<Manager>
   8:  
   9: {
  10:  
  11:     #region Constructor
  12:  
  13:  
  14:  
  15:     /// <summary>
  16:  
  17:     /// Constructor
  18:  
  19:     /// </summary>
  20:  
  21:     protected Manager()
  22:  
  23:         : base()
  24:  
  25:     {
  26:  
  27:         try
  28:  
  29:         {
  30:  
  31:             Config = Gestalt.Manager.Instance.GetConfigFile<Configuration.Configuration>("TaskScheduler");
  32:  
  33:             Workers = new List<Worker>();
  34:  
  35:             for (int x = 0; x < Config.NumberOfThreads; ++x)
  36:  
  37:             {
  38:  
  39:                 Worker TempWorker = new Worker("");
  40:  
  41:                 Workers.Add(TempWorker);
  42:  
  43:             }
  44:  
  45:         }
  46:  
  47:         catch { throw; }
  48:  
  49:     }
  50:  
  51:  
  52:  
  53:     #endregion
  54:  
  55:  
  56:  
  57:     #region Public Functions
  58:  
  59:  
  60:  
  61:     /// <summary>
  62:  
  63:     /// Starts the task manager
  64:  
  65:     /// </summary>
  66:  
  67:     /// <param name="TaskAssemblyLocation">Location of the task assembly</param>
  68:  
  69:     public void Start(string TaskAssemblyLocation)
  70:  
  71:     {
  72:  
  73:         try
  74:  
  75:         {
  76:  
  77:             Assembly TaskAssembly = Assembly.LoadFile(TaskAssemblyLocation);
  78:  
  79:             AddTasks(TaskAssembly);
  80:  
  81:             StartWorkers();
  82:  
  83:         }
  84:  
  85:         catch { throw; }
  86:  
  87:     }
  88:  
  89:  
  90:  
  91:     /// <summary>
  92:  
  93:     /// Starts the task manager
  94:  
  95:     /// </summary>
  96:  
  97:     /// <param name="TaskAssembly">The task assembly</param>
  98:  
  99:     public void Start(Assembly TaskAssembly)
 100:  
 101:     {
 102:  
 103:         try
 104:  
 105:         {
 106:  
 107:             AddTasks(TaskAssembly);
 108:  
 109:             StartWorkers();
 110:  
 111:         }
 112:  
 113:         catch { throw; }
 114:  
 115:     }
 116:  
 117:  
 118:  
 119:     /// <summary>
 120:  
 121:     /// Starts the task manager
 122:  
 123:     /// </summary>
 124:  
 125:     /// <param name="TaskAssemblies">The task assemblies</param>
 126:  
 127:     public void Start(List<Assembly> TaskAssemblies)
 128:  
 129:     {
 130:  
 131:         try
 132:  
 133:         {
 134:  
 135:             for (int x = 0; x < TaskAssemblies.Count; ++x)
 136:  
 137:             {
 138:  
 139:                 AddTasks(TaskAssemblies[x]);
 140:  
 141:             }
 142:  
 143:             StartWorkers();
 144:  
 145:         }
 146:  
 147:         catch { throw; }
 148:  
 149:     }
 150:  
 151:  
 152:  
 153:     /// <summary>
 154:  
 155:     /// Stops the tasks
 156:  
 157:     /// </summary>
 158:  
 159:     public void Stop()
 160:  
 161:     {
 162:  
 163:         try
 164:  
 165:         {
 166:  
 167:             for (int x = 0; x < Config.NumberOfThreads; ++x)
 168:  
 169:             {
 170:  
 171:                 Workers[x].Stop();
 172:  
 173:             }
 174:  
 175:         }
 176:  
 177:         catch { throw; }
 178:  
 179:     }
 180:  
 181:  
 182:  
 183:     #endregion
 184:  
 185:  
 186:  
 187:     #region Private Functions
 188:  
 189:  
 190:  
 191:     private void AddTasks(Assembly TaskAssembly)
 192:  
 193:     {
 194:  
 195:         try
 196:  
 197:         {
 198:  
 199:             List<Type> TaskTypes = Utilities.Reflection.GetTypes(TaskAssembly, "EchoNet.Task");
 200:  
 201:             for (int x = 0; x < TaskTypes.Count; )
 202:  
 203:             {
 204:  
 205:                 for (int y = 0; y < Workers.Count && x < TaskTypes.Count; ++y, ++x)
 206:  
 207:                 {
 208:  
 209:                     Task TempTask = (Task)TaskTypes[x].Assembly.CreateInstance(TaskTypes[x].FullName);
 210:  
 211:                     TempTask.Setup(TaskTypes[x].Name);
 212:  
 213:                     Workers[y].AddTask(TempTask);
 214:  
 215:                 }
 216:  
 217:             }
 218:  
 219:         }
 220:  
 221:         catch { throw; }
 222:  
 223:     }
 224:  
 225:  
 226:  
 227:     private void StartWorkers()
 228:  
 229:     {
 230:  
 231:         try
 232:  
 233:         {
 234:  
 235:             for (int x = 0; x < Config.NumberOfThreads; ++x)
 236:  
 237:             {
 238:  
 239:                 Workers[x].Start();
 240:  
 241:             }
 242:  
 243:         }
 244:  
 245:         catch { throw; }
 246:  
 247:     }
 248:  
 249:  
 250:  
 251:     #endregion
 252:  
 253:  
 254:  
 255:     #region Private Properties
 256:  
 257:  
 258:  
 259:     private List<Worker> Workers { get; set; }
 260:  
 261:     private Configuration.Configuration Config { get; set; }
 262:  
 263:     #endregion
 264:  
 265: }

The code above is the manager for everything, it handles the basic creation of worker threads, setting up the individual tasks, etc. You'll notice that it uses a Singleton class as a base class. This is just a helper class from my utility library. The other thing to note is it uses Gestalt.Net for configuration, so the Configuration class is nothing but a stub and not that interesting. Well, it does set up the number of threads to set up. But to be honest, that's not that interesting. Other than that the only thing that the manager touches are the Worker class and Task class, so let's look at the Worker class:

   1: public class Worker : Worker<bool, string>
   2:  
   3: {
   4:  
   5:     #region Constructor
   6:  
   7:  
   8:  
   9:     /// <summary>
  10:  
  11:     /// Constructor
  12:  
  13:     /// </summary>
  14:  
  15:     /// <param name="Params">Not used</param>
  16:  
  17:     public Worker(string Params)
  18:  
  19:         : base(Params)
  20:  
  21:     {
  22:  
  23:         try
  24:  
  25:         {
  26:  
  27:             Tasks = new List<Task>();
  28:  
  29:         }
  30:  
  31:         catch { throw; }
  32:  
  33:     }
  34:  
  35:  
  36:  
  37:     #endregion
  38:  
  39:  
  40:  
  41:     #region Functions
  42:  
  43:  
  44:  
  45:     /// <summary>
  46:  
  47:     /// Adds a task to 
  48:  
  49:     /// </summary>
  50:  
  51:     /// <param name="Task">Task to add</param>
  52:  
  53:     public void AddTask(Task Task)
  54:  
  55:     {
  56:  
  57:         try
  58:  
  59:         {
  60:  
  61:             lock (Tasks)
  62:  
  63:             {
  64:  
  65:                 Tasks.Add(Task);
  66:  
  67:             }
  68:  
  69:         }
  70:  
  71:         catch { throw; }
  72:  
  73:     }
  74:  
  75:  
  76:  
  77:     #endregion
  78:  
  79:  
  80:  
  81:     #region Overridden Functions
  82:  
  83:  
  84:  
  85:     protected override bool Work(string Params)
  86:  
  87:     {
  88:  
  89:         try
  90:  
  91:         {
  92:  
  93:             while (true)
  94:  
  95:             {
  96:  
  97:                 if (Stopping)
  98:  
  99:                     return true;
 100:  
 101:                 lock (Tasks)
 102:  
 103:                 {
 104:  
 105:                     for (int x = 0; x < Tasks.Count; ++x)
 106:  
 107:                     {
 108:  
 109:                         if (Tasks[x].NextRunTime < DateTime.Now)
 110:  
 111:                         {
 112:  
 113:                             Tasks[x].DoWork();
 114:  
 115:                             Tasks[x].UpdateTime(true);
 116:  
 117:                         }
 118:  
 119:                     }
 120:  
 121:                 }
 122:  
 123:                 Sleep(1000);
 124:  
 125:             }
 126:  
 127:         }
 128:  
 129:         catch { throw; }
 130:  
 131:     }
 132:  
 133:  
 134:  
 135:     #endregion
 136:  
 137:  
 138:  
 139:     #region Private Properties
 140:  
 141:  
 142:  
 143:     private List<Task> Tasks { get; set; }
 144:  
 145:  
 146:  
 147:     #endregion
 148:  
 149: }

The Worker class is the actual background thread class. Once again it uses a base class helper from my utility library. All it does is wraps the threading code (starting a thread, stopping it, etc.). The Worker thread simply holds the individual tasks, sees if they should be called, and if need be runs them. The task class is where the actual interesting code occurs:

   1: public abstract class Task:ITask
   2:  
   3: {
   4:  
   5:     #region Constructor
   6:  
   7:  
   8:  
   9:     /// <summary>
  10:  
  11:     /// Constructor
  12:  
  13:     /// </summary>
  14:  
  15:     public Task()
  16:  
  17:     {
  18:  
  19:     }
  20:  
  21:  
  22:  
  23:     #endregion
  24:  
  25:  
  26:  
  27:     #region Abstract Functions
  28:  
  29:  
  30:  
  31:     public abstract void DoWork();
  32:  
  33:  
  34:  
  35:     #endregion
  36:  
  37:  
  38:  
  39:     #region Internal Functions
  40:  
  41:  
  42:  
  43:     internal void Setup(string ClassName)
  44:  
  45:     {
  46:  
  47:         try
  48:  
  49:         {
  50:  
  51:             Config = Gestalt.Manager.Instance.GetConfigFile<TaskConfiguration>(ClassName);
  52:  
  53:             NextRunTime = Config.NextRunTime;
  54:  
  55:             if (Config.Frequency != EchoNet.Enum.RunTime.Once)
  56:  
  57:             {
  58:  
  59:                 while (NextRunTime < DateTime.Now || NextRunTime < Config.Start)
  60:  
  61:                 {
  62:  
  63:                     UpdateTime(false);
  64:  
  65:                 }
  66:  
  67:             }
  68:  
  69:             else
  70:  
  71:             {
  72:  
  73:                 if (NextRunTime < DateTime.Now)
  74:  
  75:                     NextRunTime = DateTime.Now;
  76:  
  77:                 if (NextRunTime < Config.Start)
  78:  
  79:                     NextRunTime = Config.Start;
  80:  
  81:             }
  82:  
  83:             if (NextRunTime > Config.End)
  84:  
  85:                 NextRunTime = DateTime.MaxValue;
  86:  
  87:         }
  88:  
  89:         catch { throw; }
  90:  
  91:     }
  92:  
  93:  
  94:  
  95:     internal void UpdateTime(bool Save)
  96:  
  97:     {
  98:  
  99:         try
 100:  
 101:         {
 102:  
 103:             if (Config.Frequency == EchoNet.Enum.RunTime.Hourly)
 104:  
 105:             {
 106:  
 107:                 NextRunTime = NextRunTime.AddHours(1.0d);
 108:  
 109:             }
 110:  
 111:             else if (Config.Frequency == EchoNet.Enum.RunTime.Daily)
 112:  
 113:             {
 114:  
 115:                 NextRunTime = NextRunTime.AddDays(1.0d);
 116:  
 117:             }
 118:  
 119:             else if (Config.Frequency == EchoNet.Enum.RunTime.Monthly)
 120:  
 121:             {
 122:  
 123:                 NextRunTime = NextRunTime.AddMonths(1);
 124:  
 125:             }
 126:  
 127:             else if (Config.Frequency == EchoNet.Enum.RunTime.Yearly)
 128:  
 129:             {
 130:  
 131:                 NextRunTime = NextRunTime.AddYears(1);
 132:  
 133:             }
 134:  
 135:             else if (Config.Frequency == EchoNet.Enum.RunTime.Weekly)
 136:  
 137:             {
 138:  
 139:                 NextRunTime = NextRunTime.AddDays(7.0d);
 140:  
 141:             }
 142:  
 143:             else if (Config.Frequency == EchoNet.Enum.RunTime.Once)
 144:  
 145:             {
 146:  
 147:                 NextRunTime = DateTime.MaxValue;
 148:  
 149:             }
 150:  
 151:             if (Save)
 152:  
 153:             {
 154:  
 155:                 Config.NextRunTime = NextRunTime;
 156:  
 157:                 Config.Save();
 158:  
 159:             }
 160:  
 161:         }
 162:  
 163:         catch { throw; }
 164:  
 165:     }
 166:  
 167:  
 168:  
 169:     #endregion
 170:  
 171:  
 172:  
 173:     #region Properties
 174:  
 175:  
 176:  
 177:     protected TaskConfiguration Config { get; set; }
 178:  
 179:     internal DateTime NextRunTime { get; set; }
 180:  
 181:  
 182:  
 183:     #endregion
 184:  
 185: }

OK, I lied, it's not that interesting. The Task class is nothing more than a base class and contains only a function for setup (figuring out the next time to run and getting the config info), an abstract DoWork function, and an UpdateTime function (that figures out the next time to run). And the interface that uses, just sets up the DoWork function. Now I did mention that the task loads config information. This, once again, uses Gestalt.Net to load/save our info. The base class for it looks like this:

   1: public class TaskConfiguration:Gestalt.Config<TaskConfiguration>
   2:  
   3: {
   4:  
   5:     #region Properties
   6:  
   7:  
   8:  
   9:     /// <summary>
  10:  
  11:     /// Frequency the task is run
  12:  
  13:     /// </summary>
  14:  
  15:     public virtual RunTime Frequency { get; set; }
  16:  
  17:  
  18:  
  19:     /// <summary>
  20:  
  21:     /// Start date
  22:  
  23:     /// </summary>
  24:  
  25:     public virtual DateTime Start { get; set; }
  26:  
  27:  
  28:  
  29:     /// <summary>
  30:  
  31:     /// End date
  32:  
  33:     /// </summary>
  34:  
  35:     public virtual DateTime End { get; set; }
  36:  
  37:  
  38:  
  39:     /// <summary>
  40:  
  41:     /// Next run time
  42:  
  43:     /// </summary>
  44:  
  45:     public virtual DateTime NextRunTime { get; set; }
  46:  
  47:  
  48:  
  49:     #endregion
  50:  
  51: }

That's it. All the system cares about is if this has a start/end time period, the next time it should run (which it updates itself), and the frequency it should run (hourly, daily, weekly, etc.). And that's all there is to the entire system, It's very basic but surprisingly flexible. As an example, we can set up a task by simply doing the following:

   1: public class Task1:EchoNet.Task
   2:  
   3: {
   4:  
   5:     public override void DoWork()
   6:  
   7:     {
   8:  
   9:         try
  10:  
  11:         {
  12:  
  13:             Utilities.FileManager.SaveFile("This is a test", @"C:\MyFiles\File1.txt");
  14:  
  15:         }
  16:  
  17:         catch { throw; }
  18:  
  19:     }
  20:  
  21: }

Or at least that's it for the task itself. We still need to set up the config files:

   1: public class TaskSchedulerConfig:EchoNet.Configuration.Configuration
   2:  
   3: {
   4:  
   5: }
   6:  
   7:  
   8:  
   9:  
  10:  
  11: [Gestalt.Attributes.Config(Name = "Task1")]
  12:  
  13: public class Task1Config : EchoNet.Configuration.TaskConfiguration
  14:  
  15: {
  16:  
  17:     public Task1Config()
  18:  
  19:     {
  20:  
  21:         NextRunTime = new DateTime(2010, 5, 20, 11, 11, 0);
  22:  
  23:         Start = new DateTime(1900, 1, 1);
  24:  
  25:         End = DateTime.MaxValue;
  26:  
  27:         Frequency = EchoNet.Enum.RunTime.Hourly;
  28:  
  29:     }
  30:  
  31:     protected override string ConfigFileLocation { get { return @"C:\TestWebApp\App_Data\Task.config"; } }
  32:  
  33: }

You may have noticed that the first item is simply an empty class. It inherits from our main config file for the task manager and just lets the standard 4 threads be the default. In the second class, we're inheriting from the individual task's config object. In this case, because we never named the base class, we have to set the name in the attribute (this associates this config with the Task1 class. We then set some defaults in the constructor and override the ConfigFileLocation property to set a path for the config to be saved (so we can edit it outside of the code if need be). That's it to the configuration, from here all we have to do is actually start it up:

   1: Gestalt.Manager.Instance.RegisterConfigFile(typeof(Default).Assembly);
   2: EchoNet.Manager.Instance.Start(typeof(Default).Assembly);

The two function calls are rather simple. First we start Gestalt.Net, telling it where our config classes are and then the same thing with the task manager (EchoNet is the namespace for it). In a web app this would go in the global Application_Start function. And finally we need to stop it:

   1: EchoNet.Manager.Instance.Stop();

And this would go in your Application_Stop function. Anyway, that's it. The start would load up the tasks, start the threads, and let things run. The Stop would simply stop the threads and shut things down. Really simple and rather effective, so take a look, leave feedback, and happy coding.



Comments