Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple Scheduler in C#

13 May 2013 1  
How to build a simple Scheduler in C# for Desktop and Web Applications

Introduction

One feature many Desktop and Web Applications require is to have a Scheduler. By Scheduler one means a framework which controls the parallel execution of Jobs. These Jobs can be executed only one or repeatedly. Therefore what one wishes is to have a framework which, in order to execute a Job (once or many times), it is necessary only to implement this very Job and hook it to the framework.

The goal of this article is to show how to develop this Scheduler and explaining in details the techniques and ideas behind this implementation. One may observe that, using the right design patterns and simple solutions, the outcome can be surprisingly straightforward. 

The Scheduler described here is developed in a Microsoft .NET Library Project. A Microsoft .NET Console Project uses the DLL Library in order to implement and execute a few Jobs. The next sections of this article will show how exactly this is accomplished. 

Background  

Schedulers have always been a concern for architects when developing Enterprise Applications. Quite often the necessity for such a framework occurs because automation of parallel tasks aids immensely the user´s work, because it takes out of his or hers hand repetitive activities. 

There are many ready-to-use solutions for that. On one hand, Java, for instance, provides a few schedulers, like Quartz. On the other hand, Microsoft .NET developers can take advantage of Windows Services. 

There are situations, however, that one may not be able to use these tools. For instance, one may have to deploy an ASP.NET Application in a Shared Server and the usage of Windows Services is simply not allowed.  

Even if one can use frameworks to handle scheduling events, it may be quite interesting to see how a solution can be implemented for this matter. In this case, it is a combination of using Threads adequately and implementing the Template Method Design Pattern.  

Architecture   

 In this section the architecture is discussed. Firstly, the class diagram is displayed and explained.    

The class diagram consist of the framework (in the DLL Library) along with the execution program which dispatches the Jobs. The classes Job and JobManager encomposses the the framework; It is that simple, to implement this framework. The classes RepeatableJob, RepeatableJob2 and SingleExecutionJob are Job implementations. Their names give away their main characteristics. 

The Job class is to be implemented in order to be executed, either as a repeatable, or a single-execution task. The JobManager class is responsible for gathering and executing all Jobs available (classes which extend Job). 

The RepeatableJob and RepeatableJob2 are never-ending Jobs, meaning their code is always executed given fixed time-defined intervals.  The SingleExecutionJob has its code run one. The only purpose of these Jobs, is to print a message stating they have been executed. This illustrates beautifully their behavior in a console, when the program is executed.

The remaining class, Program, simply instantiates the JobManager and run it. The JobManager executes the Jobs asynchronously.

After understanding the general idea of the provided classes of the application, one can now comprehend the main thoughts which are the core of this Scheduler architecture. The JobManager, gathers all Job implementation of the Job class. These Job implementations must be within the .NET Project which uses the DLL holding the Scheduler framework. After gathering these Jobs, each of them is started in a new thread. The Job class has the method ExecuteTask() which triggers the Job task implementation. If the Job is to be executed once, after the task is completed the Job finishes its execution and the thread dies. In case the Job is repeatable, the task is executed repeatedly in intervals provided by the Job´s implementation. Note here that the Job class has some methods which are implemented and others which must be provided by its implementation. This technique falls into the Method Template Design Pattern. 

The next sections, the most relevant parts of the code will be explained in more details so one can have a practical understanding of how to implement the Scheduler. 

Using the code  

The Job class is the obvious class to start discussing. One can have a better understanding of the framework simply by looking at it. 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
 
namespace SchedulerManager.Mechanism
{
    /// <summary>
    /// Classes which extend this abstract class are Jobs which will be
    /// started as soon as the application starts. These Jobs are executed
    /// asynchronously from the Web Application.
    /// </summary>
    public abstract class Job
    {
        /// <summary>
        /// Execute the Job itself, one ore repeatedly, depending on
        /// the job implementation.
        /// </summary>
        public void ExecuteJob()
        {
            if (IsRepeatable())
            {
                // execute the job in intervals determined by the methd
                // GetRepetionIntervalTime()
                while (true)
                {
                    DoJob();
                    Thread.Sleep(GetRepetitionIntervalTime());
                }
            }
            // since there is no repetetion, simply execute the job
            else
            {
                DoJob();
            }
        }
 
        /// <summary>
        /// If this method is overriden, on can get within the job
        /// parameters set just before the job is started. In this
        /// situation the application is running and the use may have
        /// access to resources which he/she has not during the thread
        /// execution. For instance, in a web application, the user has
        /// no access to the application context, when the thread is running.
        /// Note that this method must not be overriden. It is optional.
        /// </summary>
        /// <returns>Parameters to be used in the job.</returns>
        public virtual Object GetParameters()
        {
            return null;
        }
 
        /// <summary>
        /// Get the Job´s Name. This name uniquely identifies the Job.
        /// </summary>
        /// <returns>Job´s name.</returns>
        public abstract String GetName();
 
        /// <summary>
        /// The job to be executed.
        /// </summary>
        public abstract void DoJob();
 
        /// <summary>
        /// Determines whether a Job is to be repeated after a
        /// certain amount of time.
        /// </summary>
        /// <returns>True in case the Job is to be repeated, false otherwise.</returns>
        public abstract bool IsRepeatable();
 
        /// <summary>
        /// The amount of time, in milliseconds, which the Job has to wait until it is started
        /// over. This method is only useful if IJob.IsRepeatable() is true, otherwise
        /// its implementation is ignored.
        /// </summary>
        /// <returns>Interval time between this job executions.</returns>
        public abstract int GetRepetitionIntervalTime();
    }
} 

This class is well commented so its reading is quite facilitated. The idea here is to provide everything a Job needs to be executed. The methods´ explanation will be given below: 

  • DoJob() - Here is to be provided the task execution itself. This means all the work to be done has to be put inside this method. 
  • IsRepeatable() - Determine whether the task is to be repeated or not. 
  • GetRepetitionIntervalTime() - Return the interval, in milliseconds, which the Job has to wait until it is to be executed again, in case the Job is repeatable, obviously.
  • GetName() - Uniquely identifies the Job. It is very important that this name indeed is unique among the implemented Jobs in the assembly holding them, otherwise unexpected behavior is to occur.
  • GetParameters() - This method is to be implemented when one wishes to pass parameters to the task execution. In order to access the entered parameters, only simply has to call this method within the task implementation - DoJob() method.
  • ExecuteJob() - This method executes the task itself. It calls the DoJob() method. Note that in case the method is to be run repeatedly, the DoJob() is executed in a loop, otherwise it is called only one. Here is where the Method Template Design Pattern is applied. The ExecuteJob() method is executed based on its class´s method´s implementation. 

The next class to be understood is the JobManager, which is displayed below: 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using log4net;
using SchedulerManager.Log4Net;
 
namespace SchedulerManager.Mechanism
{
    /// <summary>
    /// Job mechanism manager.
    /// </summary>
    public class JobManager
    {
        private ILog log = LogManager.GetLogger(Log4NetConstants.SCHEDULER_LOGGER);
 
        /// <summary>
        /// Execute all Jobs.
        /// </summary>
        public void ExecuteAllJobs()
        {
            log.Debug("Begin Method");
 
            try
            {
                // get all job implementations of this assembly.
                IEnumerable<Type> jobs = GetAllTypesImplementingInterface(typeof(Job));
                // execute each job
                if (jobs != null && jobs.Count() > 0)
                {
                    Job instanceJob = null;
                    Thread thread = null;
                    foreach (Type job in jobs)
                    {
                        // only instantiate the job its implementation is "real"
                        if (IsRealClass(job))
                        {
                            try
                            {
                                // instantiate job by reflection
                                instanceJob = (Job)Activator.CreateInstance(job);
                                log.Debug(String.Format(
                                  "The Job \"{0}\" has been instantiated successfully.", 
                                  instanceJob.GetName()));
                                // create thread for this job execution method
                                thread = new Thread(new ThreadStart(instanceJob.ExecuteJob));
                                // start thread executing the job
                                thread.Start();
                                log.Debug(String.Format(
                                  "The Job \"{0}\" has its thread started successfully.", 
                                  instanceJob.GetName()));
                            }
                            catch (Exception ex)
                            {
                                log.Error(String.Format("The Job \"{0}\" could not " + 
                                  "be instantiated or executed.", job.Name), ex);
                            }
                        }
                        else
                        {
                            log.Error(String.Format(
                              "The Job \"{0}\" cannot be instantiated.", job.FullName));
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                log.Error("An error has occured while instantiating " + 
                  "or executing Jobs for the Scheduler Framework.", ex);
            }
 
            log.Debug("End Method");
        }
 
        /// <summary>
        /// Returns all types in the current AppDomain implementing the interface or inheriting the type. 
        /// </summary>
        private IEnumerable<Type> GetAllTypesImplementingInterface(Type desiredType)
        {
            return AppDomain
                .CurrentDomain
                .GetAssemblies()
                .SelectMany(assembly => assembly.GetTypes())
                .Where(type => desiredType.IsAssignableFrom(type));
 
        }
 
        /// <summary>
        /// Determine whether the object is real - non-abstract, non-generic-needed, non-interface class.
        /// </summary>
        /// <param name="testType">Type to be verified.</param>
        /// <returns>True in case the class is real, false otherwise.</returns>
        public static bool IsRealClass(Type testType)
        {
            return testType.IsAbstract == false
                && testType.IsGenericTypeDefinition == false
                && testType.IsInterface == false;
        }
    }
} 

Again, this class is well commented in order to ease the understanding. Have a look at the ExecuteAllJobs() method. This method, gathers all Job implementations from the assembly its being executed and run them in separate threads. Observe that this solution is quite simple. This solution does not have the dangers of deadlocks or complicated thread interactions. Furthermore, since each thread runs independently, it is very easy to debug and find errors in this framework.

One last think to be observed are the Jobs implementations. Below are displayed the SimgleExecutionJob and the  RepeatableJob ones. 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SchedulerManager.Mechanism;
 
namespace SchedulerConsoleApp.Jobs
{
    /// <summary>
    /// A simple job which is executed only once.
    /// </summary>
    class SimgleExecutionJob : Job
    {
        /// <summary>
        /// Get the Job Name, which reflects the class name.
        /// </summary>
        /// <returns>The class Name.</returns>
        public override string GetName()
        {
            return this.GetType().Name;
        }
 
        /// <summary>
        /// Execute the Job itself. Just print a message.
        /// </summary>
        public override void DoJob()
        {
            System.Console.WriteLine(String.Format("The Job \"{0}\" was executed.", 
                                       this.GetName()));
        }
 
        /// <summary>
        /// Determines this job is not repeatable.
        /// </summary>
        /// <returns>Returns false because this job is not repeatable.</returns>
        public override bool IsRepeatable()
        {
            return false;
        }
 
        /// <summary>
        /// In case this method is executed NotImplementedException is thrown
        /// because this method is not to to be used. This method is never used
        /// because it serves the purpose of stating the interval of which the job
        /// will be executed repeatedly. Since this job is a single-execution one,
        /// this method is rendered useless.
        /// </summary>
        /// <returns>Returns nothing because this method is not to be used.</returns>
        public override int GetRepetitionIntervalTime()
        {
            throw new NotImplementedException();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SchedulerManager.Mechanism;
 
namespace SchedulerConsoleApp.Jobs
{
    /// <summary>
    /// A simple repeatable Job.
    /// </summary>
    class RepeatableJob : Job
    {
        /// <summary>
        /// Counter used to count the number of times this job has been
        /// executed.
        /// </summary>
        private int counter = 0;
        
        /// <summary>
        /// Get the Job Name, which reflects the class name.
        /// </summary>
        /// <returns>The class Name.</returns>
        public override string GetName()
        {
            return this.GetType().Name;
        }
 
        /// <summary>
        /// Execute the Job itself. Just print a message.
        /// </summary>
        public override void DoJob()
        {
            System.Console.WriteLine(String.Format(
              "This is the execution number \"{0}\" of the Job \"{1}\".", 
              counter.ToString(), this.GetName()));
            counter++;
        }
 
        /// <summary>
        /// Determines this job is repeatable.
        /// </summary>
        /// <returns>Returns true because this job is repeatable.</returns>
        public override bool IsRepeatable()
        {
            return true;
        }
 
        /// <summary>
        /// Determines that this job is to be executed again after
        /// 1 sec.
        /// </summary>
        /// <returns>1 sec, which is the interval this job is to be
        /// executed repeatadly.</returns>
        public override int GetRepetitionIntervalTime()
        {
            return 1000;
        }
    }
}

Note how straightforward they are: the SimgleExecutionJob provides its identifier based on its class name, implements the task, which is to print a message, and tells the Job is not repeatable. The RepeatableJob, does state the task is repetitive, provides the interval of execution, gives the class name to use as its identifier, and defines its task as a simple message printing. 

Compiling and Running the Code 

When one opens the code and tries to compile it, errors are displayed stating that there is no log4net libraries. This occurs because it is not allowed to upload .dll files in CodeProject articles, thus the log4net.dll was removed. Therefore, to fix the Solution Setup and build it correctly, download the log4net library clicking here.  After that, create a sub-directory called libraries in the folder SchedulerManager-noexe. Inside the directory libraries copy the log4net.dll file one has just downloaded. Finally, refresh the SchedulerManager project references and you should see that the log4net reference has no warning flag. Compile the solution and run the SchedulerConsoleApp project. 

 Discussion  

Firstly, note the simplicity of this solution, as stated before. Simply put, one gathers all Job implementations and executes them in separate threads. This straightforwardness allows one to easily add more functionalities to this framework.

In the subject of new features, one may consider having the Job´s requirements in a database. For instance, whether or not the job is repeatable, interval of execution, could be all stored in a database, each row identified by its unique name. This makes maintenance easier because in order to change their parameters execution no code change is necessary, simply database changes are needed. 

Other features, such as, interface to handle the Job Management, the ability to run, pause and cancel a Job execution could also be implemented. Note however, that while these features seem nice and fancy, in most of applications there are rarely needed.

Many people argue that dealing with threads in Web Applications can be very dangerous because it may jeopardize the Application Sever. In this author´s experience, it could not be further from the truth, at least when one is working with Internet Information Services.  

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here