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

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:

    /// <summary>

    /// Manager for the task scheduler

    /// </summary>

    public class Manager:Singleton<Manager>

    {

        #region Constructor

 

        /// <summary>

        /// Constructor

        /// </summary>

        protected Manager()

            : base()

        {

            try

            {

                Config = Gestalt.Manager.Instance.GetConfigFile<Configuration.Configuration>("TaskScheduler");

                Workers = new List<Worker>();

                for (int x = 0; x < Config.NumberOfThreads; ++x)

                {

                    Worker TempWorker = new Worker("");

                    Workers.Add(TempWorker);

                }

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Public Functions

 

        /// <summary>

        /// Starts the task manager

        /// </summary>

        /// <param name="TaskAssemblyLocation">Location of the task assembly</param>

        public void Start(string TaskAssemblyLocation)

        {

            try

            {

                Assembly TaskAssembly = Assembly.LoadFile(TaskAssemblyLocation);

                AddTasks(TaskAssembly);

                StartWorkers();

            }

            catch { throw; }

        }

 

        /// <summary>

        /// Starts the task manager

        /// </summary>

        /// <param name="TaskAssembly">The task assembly</param>

        public void Start(Assembly TaskAssembly)

        {

            try

            {

                AddTasks(TaskAssembly);

                StartWorkers();

            }

            catch { throw; }

        }

 

        /// <summary>

        /// Starts the task manager

        /// </summary>

        /// <param name="TaskAssemblies">The task assemblies</param>

        public void Start(List<Assembly> TaskAssemblies)

        {

            try

            {

                for (int x = 0; x < TaskAssemblies.Count; ++x)

                {

                    AddTasks(TaskAssemblies[x]);

                }

                StartWorkers();

            }

            catch { throw; }

        }

 

        /// <summary>

        /// Stops the tasks

        /// </summary>

        public void Stop()

        {

            try

            {

                for (int x = 0; x < Config.NumberOfThreads; ++x)

                {

                    Workers[x].Stop();

                }

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Private Functions

 

        private void AddTasks(Assembly TaskAssembly)

        {

            try

            {

                List<Type> TaskTypes = Utilities.Reflection.GetTypes(TaskAssembly, "EchoNet.Task");

                for (int x = 0; x < TaskTypes.Count; )

                {

                    for (int y = 0; y < Workers.Count && x < TaskTypes.Count; ++y, ++x)

                    {

                        Task TempTask = (Task)TaskTypes[x].Assembly.CreateInstance(TaskTypes[x].FullName);

                        TempTask.Setup(TaskTypes[x].Name);

                        Workers[y].AddTask(TempTask);

                    }

                }

            }

            catch { throw; }

        }

 

        private void StartWorkers()

        {

            try

            {

                for (int x = 0; x < Config.NumberOfThreads; ++x)

                {

                    Workers[x].Start();

                }

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Private Properties

 

        private List<Worker> Workers { get; set; }

        private Configuration.Configuration Config { get; set; }

        #endregion

    }

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:

    public class Worker : Worker<bool, string>

    {

        #region Constructor

 

        /// <summary>

        /// Constructor

        /// </summary>

        /// <param name="Params">Not used</param>

        public Worker(string Params)

            : base(Params)

        {

            try

            {

                Tasks = new List<Task>();

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Functions

 

        /// <summary>

        /// Adds a task to 

        /// </summary>

        /// <param name="Task">Task to add</param>

        public void AddTask(Task Task)

        {

            try

            {

                lock (Tasks)

                {

                    Tasks.Add(Task);

                }

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Overridden Functions

 

        protected override bool Work(string Params)

        {

            try

            {

                while (true)

                {

                    if (Stopping)

                        return true;

                    lock (Tasks)

                    {

                        for (int x = 0; x < Tasks.Count; ++x)

                        {

                            if (Tasks[x].NextRunTime < DateTime.Now)

                            {

                                Tasks[x].DoWork();

                                Tasks[x].UpdateTime(true);

                            }

                        }

                    }

                    Sleep(1000);

                }

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Private Properties

 

        private List<Task> Tasks { get; set; }

 

        #endregion

    }

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:

    public abstract class Task:ITask

    {

        #region Constructor

 

        /// <summary>

        /// Constructor

        /// </summary>

        public Task()

        {

        }

 

        #endregion

 

        #region Abstract Functions

 

        public abstract void DoWork();

 

        #endregion

 

        #region Internal Functions

 

        internal void Setup(string ClassName)

        {

            try

            {

                Config = Gestalt.Manager.Instance.GetConfigFile<TaskConfiguration>(ClassName);

                NextRunTime = Config.NextRunTime;

                if (Config.Frequency != EchoNet.Enum.RunTime.Once)

                {

                    while (NextRunTime < DateTime.Now || NextRunTime < Config.Start)

                    {

                        UpdateTime(false);

                    }

                }

                else

                {

                    if (NextRunTime < DateTime.Now)

                        NextRunTime = DateTime.Now;

                    if (NextRunTime < Config.Start)

                        NextRunTime = Config.Start;

                }

                if (NextRunTime > Config.End)

                    NextRunTime = DateTime.MaxValue;

            }

            catch { throw; }

        }

 

        internal void UpdateTime(bool Save)

        {

            try

            {

                if (Config.Frequency == EchoNet.Enum.RunTime.Hourly)

                {

                    NextRunTime = NextRunTime.AddHours(1.0d);

                }

                else if (Config.Frequency == EchoNet.Enum.RunTime.Daily)

                {

                    NextRunTime = NextRunTime.AddDays(1.0d);

                }

                else if (Config.Frequency == EchoNet.Enum.RunTime.Monthly)

                {

                    NextRunTime = NextRunTime.AddMonths(1);

                }

                else if (Config.Frequency == EchoNet.Enum.RunTime.Yearly)

                {

                    NextRunTime = NextRunTime.AddYears(1);

                }

                else if (Config.Frequency == EchoNet.Enum.RunTime.Weekly)

                {

                    NextRunTime = NextRunTime.AddDays(7.0d);

                }

                else if (Config.Frequency == EchoNet.Enum.RunTime.Once)

                {

                    NextRunTime = DateTime.MaxValue;

                }

                if (Save)

                {

                    Config.NextRunTime = NextRunTime;

                    Config.Save();

                }

            }

            catch { throw; }

        }

 

        #endregion

 

        #region Properties

 

        protected TaskConfiguration Config { get; set; }

        internal DateTime NextRunTime { get; set; }

 

        #endregion

    }

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:

    public class TaskConfiguration:Gestalt.Config<TaskConfiguration>

    {

        #region Properties

 

        /// <summary>

        /// Frequency the task is run

        /// </summary>

        public virtual RunTime Frequency { get; set; }

 

        /// <summary>

        /// Start date

        /// </summary>

        public virtual DateTime Start { get; set; }

 

        /// <summary>

        /// End date

        /// </summary>

        public virtual DateTime End { get; set; }

 

        /// <summary>

        /// Next run time

        /// </summary>

        public virtual DateTime NextRunTime { get; set; }

 

        #endregion

    }

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:

    public class Task1:EchoNet.Task

    {

        public override void DoWork()

        {

            try

            {

                Utilities.FileManager.SaveFile("This is a test", @"C:\MyFiles\File1.txt");

            }

            catch { throw; }

        }

    }

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

    public class TaskSchedulerConfig:EchoNet.Configuration.Configuration

    {

    }

 

    [Gestalt.Attributes.Config(Name = "Task1")]

    public class Task1Config : EchoNet.Configuration.TaskConfiguration

    {

        public Task1Config()

        {

            NextRunTime = new DateTime(2010, 5, 20, 11, 11, 0);

            Start = new DateTime(1900, 1, 1);

            End = DateTime.MaxValue;

            Frequency = EchoNet.Enum.RunTime.Hourly;

        }

        protected override string ConfigFileLocation { get { return @"C:\TestWebApp\App_Data\Task.config"; } }

    }

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:

            Gestalt.Manager.Instance.RegisterConfigFile(typeof(Default).Assembly);

            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:

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.

kick it on DotNetKicks.com   Shout it
Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkListEmail

Posted by: James Craig
Posted on: 5/28/2010 at 8:30 AM
Tags: , , ,
Categories: ASP.Net | C#
Post Information: Permalink | Comments (1) | Post RSSRSS comment feed

How to Add an Error Message to a ValidationSummary Progrommatically

For a while now I had been annoyed at the fact that there is no easy way to add an error message to a ValidationSummary control... Or at least I didn't think that there was, but it turns out that it's quite simple:

            RequiredFieldValidator Validator = new RequiredFieldValidator();
            Validator.ErrorMessage = "This is your error message";
            Validator.ValidationGroup = "Group1";
            Validator.IsValid = false;
            Validator.Visible = false;
            Page.Form.Controls.Add(Validator);

The error message and the validation group needs to be changed but basically this just creates a required field validator. It then says that the validator isn't valid and makes it invisible. However since we have a ValidationSummary object, it will show up there (assuming we set up the validation group properly). That's all there is to it. Anyway, I hope this helps out someone so you're not banging your head against a wall like I was. So try it out, leave feedback, and happy coding.

kick it on DotNetKicks.com   Shout it
Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkListEmail

Posted by: James Craig
Posted on: 7/21/2009 at 10:15 AM
Tags: , , ,
Categories: ASP.Net
Post Information: Permalink | Comments (0) | Post RSSRSS comment feed

Copy to Clipboard AJAX Control

This is actually the same code that I used for the BlogEngine.Net extension that I created a while back. However I needed it to be a bit more generic (not BlogEngine specific) and take the code from a text box so I ended up creating an AJAX extender:

CopyToClipboardExtender.zip (2.07 kb)

It's rather basic. The targetID is the link you want them to click on to copy the text. The CopyID is the text box's ID. It most likely will not work in Firefox, etc. and is really only IE specific. However it's better than nothing. 

As far as setting it up, sometimes I get a bit lazy when it comes to explaining how to use some of the code on here. In the case of the AJAX controls, I usually leave out the fact that you need to download the AJAX Control Toolkit. Once you have that and you've added the templates, etc. like it says in the setup, you need to create an ASP.Net AJAX Control Project (I've used the name of AJAXControls, so if you use something different you'll need to potentially change some of the code to point to the correct namespace). From there, you add the code (making sure to have the js file as an embedded resource). Compile and you're ready to go...

Now that jQuery is going to be the norm, the setup process will probably change in the future. Plus I plan on packaging these up at some point like the utilities. For now though I leave you with the code. So try it out, leave feedback, and happy coding.

kick it on DotNetKicks.com   Shout it
Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkListEmail

Posted by: James Craig
Posted on: 12/12/2008 at 10:00 AM
Tags: , , ,
Categories: AJAX | ASP.Net | Web Design
Post Information: Permalink | Comments (0) | Post RSSRSS comment feed