.NET managed scheduled task runner

Scheduled managed code execution in .NET

Sometime ago I wrote an article about Windows Scheduled Task vs Windows Service where I compared and listed benefits and drawbacks of using scheduled tasks in Windows and Windows Services for executing certain operation which repeats in some time span.

However idea of executing tasks in a single Windows Service is very useful if you need to execute task in a short time span plus you have a full control including logging and other fine grained customization in case you write the task in your code.

By including dynamic loading of tasks which can be separated in it's own DLLs, you get one really customizable and extendibles solution with only one Windows Service which can handle multiple tasks end execute them when they are schedule for executing.

The following implementation lets you do exactly what is mentioned above. It allows you to write the tasks as a separate projects, compile them in their own DLLs and then load them dynamically and execute based on the properties set in the task class itself.

To achieve running any task developed we need to follow certain structure. To do so, there needs to be interface common for all the tasks which will be handled by the runner class which will schedule and initiate task running.

using System;

namespace Scheduler
{
    public interface ISchedule
    {
        /// <summary>
        /// Time when the task will start
        /// </summary>
        DateTime StartTime { get; }

        /// <summary>
        /// Time when task will stop to be executed by the ScheuleRunner class
        /// </summary>
        DateTime? EndTime {get;}

        /// <summary>
        /// Interval for repeating the task
        /// </summary>
        int PeriodSeconds { get; }

        /// <summary>
        /// If Enabled is true task will be executed otherwise, it will be skipped
        /// </summary>
        bool Enabled { get; }

        /// <summary>
        /// Creates new instance intsed of usinf Activator or Reflection
        /// </summary>
        /// <returns>New instance of the clas</returns>
        ISchedule GetInstance();

        /// <summary>
        /// Performs scheduled action
        /// </summary>
        void Start();
    }
}

    

Now this interface represents a mold for every task we would like to execute on the time scheduled. Each task class needs to implement this interface in order to be picked up by the runner class. The runner class needs to contain all the logic for the task executing.

One thing that might be confusing is the method GetInstance. This method is actually returning the new instance of the schedule class. This is done to avoid reflection during the time of executing because it might take certain amount of time for creating the instance through reflection, so instead of reflection each implementation of this interface needs to return it's new instance.

Schedule Arch

Since every task needs to be scheduled for executing I added one more helper class which will contain the actual task class instanceĀ and the time task will run next time.

using System;

namespace Scheduler
{
    public class ScheduleUnit
    {
        private ISchedule schedule;

        /// <summary>
        /// Instance of the schedule task
        /// </summary>
        public ISchedule Schedule
        {
            get
            {
                return this.schedule;
            }
        }

        /// <summary>
        /// Next time the task will run
        /// </summary>
        public DateTime StartAt
        {
            get; set;
        }

        public ScheduleUnit(ISchedule schedule, DateTime startAt)
        {
            this.schedule = schedule;
            this.StartAt = startAt;
        }
    }
}

    

Now the runner class which will contain the complete logic for running needs to be developed. Ideally this code will be used inside the Windows Service, but to have it available for any other type of application, for example for Windows Forms application, additional background Thread/Task needs to be involved in order not to block the main task and therefore not to affect it's host application.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;
using System.ComponentModel;

namespace Scheduler
{
    public class ScheduleRunner
    {
        private string schedulesFolder;
        private string schedulesNameStartWith;
        private const string SCHEDULE_NAME_START = "Schedule.";
        private List<ScheduleUnit> schedules;
        private object locker = new object();

        /// <summary>
        /// List of schedule units loaded from the task folder
        /// </summary>
        public IEnumerable<ScheduleUnit> Schedules
        {
            get
            {
                return this.schedules;
            }
        }

        /// <summary>
        /// Folder which will contain the task libraries
        /// </summary>
        public string SchedulesFolder
        {
            get
            {
                return this.schedulesFolder;
            }
        }

        /// <summary>
        /// String value with which task libraries file names start with
        /// </summary>
        public string SchedulesNameStartWith
        {
            get
            {
                return this.schedulesNameStartWith;
            }
        }



        public ScheduleRunner()
        {
            this.schedulesFolder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Schedules");
            this.schedulesNameStartWith = SCHEDULE_NAME_START;
            this.Initialize();
        }

        public ScheduleRunner(string schedulesFolder, string schedulesNameStartWith = SCHEDULE_NAME_START)
        {
            this.schedulesFolder = schedulesFolder;
            this.schedulesNameStartWith = schedulesNameStartWith;
            this.Initialize();
        }

        private void Initialize()
        {

            if (Directory.Exists(this.schedulesFolder))
            {
                if (LoadSchedules() > 0)
                {
                    Task.Run(() =>
                    {
                        while (true)
                        {
                            foreach (ScheduleUnit schedule in this.Schedules)
                            {
                                if ((DateTime.Now - schedule.StartAt).Seconds == 0 && (schedule.Schedule.EndTime == null || schedule.Schedule.EndTime > DateTime.Now))
                                {
                                    lock (locker)
                                    {
                                        schedule.StartAt = schedule.StartAt.AddSeconds(schedule.Schedule.PeriodSeconds);
                                    }

                                    Task.Run(() =>
                                    {
                                        schedule.Schedule.GetInstance().Start();
                                    });
                                }
                            }

                        }
                    });
                }
            }
            else
            {
                throw new DirectoryNotFoundException(string.Format("Folder \"{0}\" does not exists", this.SchedulesFolder));
            }
        }


        private int LoadSchedules()
        {
            
            foreach (string file in Directory.GetFiles(this.SchedulesFolder).Where(f => Path.GetFileName( f).StartsWith(this.schedulesNameStartWith, StringComparison.InvariantCultureIgnoreCase) && Path.GetFileName(f).EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)))
            {
                foreach (Type scheduleType in Assembly.LoadFile(file).GetTypes().Where(t => typeof(ISchedule).IsAssignableFrom(t)))
                {
                    if (this.schedules == null)
                    {
                        this.schedules = new List<ScheduleUnit>();
                    }

                    ISchedule schedule = Activator.CreateInstance(scheduleType) as ISchedule;
                    if (schedule != null && schedule.Enabled)
                    {
                        this.schedules.Add(new ScheduleUnit(schedule, schedule.StartTime));
                    }


                }
            }
            return this.schedules == null ? 0 : this.schedules.Count;
        }
    }
}

    

Because each task can take certain amount of time which may affect next task for executing from the list, method Start of the ISchedule class needs to be executed from the separate Thread/Task as well.

Schedule Thread

Since we mentioned tasks should be able to run from the separate DLLs, we need to tell the runner from which folder to load DLLs and of course what will be the string with which DLL name will start to avoid loading of unnecessary DLLs which might be in that folder.

Now the runner class will pickup all the DLLs from the target folder and create instances of every class that implements ISchedule, create instances and add them to a collection from which it will schedule its run.

Now for test we need to create a separate project which will contain one class that implements ISchedule. We will make a simple class which just prints out something to console

using System;
using Scheduler;

namespace Schedule.Schedule1
{
    class BluePrinter : Scheduler.ISchedule
    {
        public bool Enabled
        {
            get
            {
                return true;
            }
        }

        public DateTime? EndTime
        {
            get
            {
                return null;
            }
        }

        public int PeriodSeconds
        {
            get
            {
                return 3;
            }
        }

        public DateTime StartTime
        {
            get
            {
                return DateTime.Now;
            }
        }

        public ISchedule GetInstance()
        {
            return new BluePrinter();
        }

        public void Start()
        {
            Console.WriteLine(string.Format("{0} {1}", DateTime.Now, this.GetType().Name.ToUpper()));
        }
    }
}

    

To test the schedule running we need to instantiate the runner class. To keep things simple, I decided to use simple console application.

using System;

namespace ScheduleTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var runner = new Scheduler.ScheduleRunner();
            Console.ReadLine();
        }
    }
}

    

Since all the logic of the runner is in the constructor, all you actually need to to is to create an instance. The rest is on the runner class to load, manage and execute schedules logic.

Test code mentioned here is included in the attachment of this article so you can download it and test it out of the box without needing to create new projects or setting up the output build path for your test schedule separate projects.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article