Click here to Skip to main content
Click here to Skip to main content

ΝUnit Inspired Task Runner

, 26 Nov 2010
Rate this:
Please Sign up or sign in to vote.
Creating NUnit like task sequences for routine application processes

Introduction

A common and downright unglamorous requirement in many business applications is to run sequences of tasks for things like data import or export and other daily processing. These task sequences don't change very often and detailed knowledge of them leaks from the organisation as people move on. This means that it can be difficult to update them without introducing new faults. This is particularly true if task order has to be changed or a new task introduced part way through a sequence.

By providing an easy to use way of creating task sequences that allows us to group related tasks and any state information they might require, set task execution order at design time and add new tasks to, or remove tasks from, a sequence without having to understand or amend any control logic and without having to move large chunks of code around it ought to be possible to reduce the effort required to maintain such sequences and, perhaps, reduce the likelihood of introducing new faults when amending a task sequence.

The starting point for this solution was the realisation that by identifying methods in a sequence with a custom attribute much as NUnit does it would be possible to write a task controller that could locate and run tasks without additional information other than that available at sequence design time.

The solution as it currently stands requires only a modicum of reflection and three classes.

Class Comment
Task A custom attribute that allows a developer to mark a method as a task
TaskInvoker A custom attribute that allows a developer to mark a method as a custom task caller.
TaskGroup Abstract class. An implementation of which, with task methods, forms an executable sequence of tasks.

Using the Code

Outline

Having added a reference to the TaskRunner DLL to his or her project, the developer derives a class from TaskGroup and adds a method to the class for each task that is to be carried out. Each of these methods is marked with a Task attribute which allows the developer to assign the method a position in the execution order and an identity that can be queried at run time.

The default signature for a Task method is:

void <name>(void)

It is possible to create TaskGroups with methods that have a signature other than the default. To do this, the developer must add a custom call or hook method marked with the TaskInvoker attribute to the derived class.

To run the TaskGroup, an instance is created and its Execute method called.

Class: Task

Property Type Comment
ExecuteOrder integer Optional. If not specified then tasks will be executed in discovery order. If specified, then must be specified for all tasks in the taskgroup. Cannot have some tasks with and some tasks without. Duplicate values are not allowed, but there is no requirement to use regular intervals.
Identity integer Optional. Allows for runtime identification of a task.
Ignore bool Optional. Exclude a task from the sequence.
Comment String Optional. Brief description of task's purpose.

Example 1

No task order specified. Tasks executed in discovery (source code) order.

[Task()]
public void ImpTaskFour()
{
  Console.WriteLine("Implicitly ordered Task Group (Task Four)");
}    

[Task()]
public void ImpTaskTwo()
{
  Console.WriteLine("Implicitly ordered Task Group (Task Two)");
}    

[Task()]
public void ImpTaskThree()
{
  Console.WriteLine("Implicitly ordered Task Group (Task Three)");
}    

[Task()]
public void ImpTaskOne()
{
  Console.WriteLine("Implicitly ordered Task Group (Task One)");
} 

Example 2

Task order specified. Tasks will be executed in the order shown regardless of discovery order. Note that task two will not be run.

[Task(ExecuteOrder=2, Ignore=true)]
public void ExpTaskTwo()
{
  Console.WriteLine("Explicitly ordered Task Group (Task Two)");
} 

[Task(ExecuteOrder=3)]
public void ExpTaskThree()
{
  Console.WriteLine("Explicitly ordered Task Group (Task Three)");
}    

[Task(ExecuteOrder=1)]
public void ExpTaskOne()
{
  Console.WriteLine("Explicitly ordered Task Group (Task One)");
}  

Example 3

Tasks requiring a TaskInvoker intercept with order and identity specified. Tasks will be executed in the order shown regardless of discovery order, but the developer must provide a TaskInvoker method.

This example uses an enumeration to make runtime identification less error prone.

[Task(ExecuteOrder=2, 
      Identity=(int)TaskIdent.SimpleString,
      Comment="Return unaltered value as text.")]
public string SimpleString(int value)
{
  return value.ToString();
}

[Task(ExecuteOrder=1, 
      Identity=(int)TaskIdent.SquareString, 
      Comment="Return square of value as text.")]
public string SquareString(int value)
{
  value *= value;
  return value.ToString();
}

Class: TaskInvoker

Property Type Comment
Comment String Optional. Brief description of TaskInvoker's purpose.

A TaskInvoker need not be terribly complex, all it has to do is make sure that the methods in the TaskGroup are called in the correct way. The example below shows a number of possible techniques that can be used.

Example

[TaskInvoker(Comment="Call methods that accept an int and return a string.")]
public void CallMethod(MethodInfo mi)
{
  // Identify the task to be executed.
  Task task = TaskAttributes(mi);
  
  // Demonstrate conditional execution based on task execution order.
  if (task.ExecuteOrder == 1)
    Console.WriteLine("First task to execute is {0}", task.Comment);
    
  // Demonstrate conditional execution based on task identity.
  if (task.Identity == (int)TaskIdent.SimpleString)
    Console.Write("Simple ");
  else
    Console.Write("Square ");
    
  // Invoke task according to signature and return type using a standard
  // bit of reflection.
  string retVal = (string)mi.Invoke(this, new object[]{param});
  
  Console.WriteLine(retVal);
}

You'll note that the TaskInvoker method has to have the signature...

void <name>(MethodInfo)

...and that the example expects all methods in its TaskGroup to have the same signature. However as it is possible to read a task's ExecuteOrder and Identity attributes and execute branches accordingly, this isn't a strict requirement.

A TaskGroup may have only one TaskInvoker. If more than one is written, then the last one found at TaskGroup instantiation is used.

Class: TaskGroup

This is an abstract class so the developer has no choice but to create a derived class.

Property Type Comment
CustomCall MethodInfo Null or a MethodInfo describing a TaskInvoker

Method Visibility Comment
Execute public Runs all Tasks in the TaskGroup
findTaskMethods protected Locates all Tasks and TaskInvoker in the TaskGroup. Run at TaskGroup instantiation.
TaskAttributes protected Retrieves the Task attribute, if any, for a method.

Example 1

It doesn't get much easier than this. The following is a complete runnable TaskGroup. If we were not concerned with task order, then we could simplify it further by dropping the ExecuteOrder specifications.

internal class TaskGroupExplicitlyOrdered : TaskGroup
  {
    [Task(ExecuteOrder=2)]
    public void ExpTaskTwo()
    {
      Console.WriteLine("Explicitly ordered Task Group (Task Two)");
    }    

    [Task(ExecuteOrder=3)]
    public void ExpTaskThree()
    {
      Console.WriteLine("Explicitly ordered Task Group (Task Three)");
    }    

    [Task(ExecuteOrder=1)]
    public void ExpTaskOne()
    {
      Console.WriteLine("Explicitly ordered Task Group (Task One)");
    }    

    [Task(ExecuteOrder=4)]
    public void ExpTaskFour()
    {
      Console.WriteLine("Explicitly ordered Task Group (Task Four)");
    } 
  }

Example 2

The following is also a complete runnable TaskGroup, but demonstrates retrieval and use of Task attributes as well as a custom TaskInvoker.

internal class TaskGroupCustomCall : TaskGroup
  {

    // To demonstrate task identification.
    private enum TaskIdent
    {
      SimpleString = 0,
      SquareString = 1
    }

    private int param = 0;

    // To demonstrate that we can provide and use setup params.
    // Note the call to the base constructor.
    public TaskGroupCustomCall(int someValue) : base()
    {
      param = someValue;
    }

    // We have a case where the default task method signature void name(void) 
    // doesn't meet our needs. 
    // So we mark a method as a task invoker and hand it the responsibility of 
    // calling each method in the task group.

    [TaskInvoker(Comment="Call methods that accept an int and return a string.")]
    public void CallMethod(MethodInfo mi)
    {

      // Identify the task to be executed.
      Task task = TaskAttributes(mi);

      // Demonstrate conditional execution based on task execution order.
      if (task.ExecuteOrder == 1)
        Console.WriteLine("First task to execute is {0}", task.Comment);

      // Demonstrate conditional execution based on task identity.
      if (task.Identity == (int)TaskIdent.SimpleString)
        Console.Write("Simple ");
      else
        Console.Write("Square ");

      // Invoke task according to signature and return type using a standard
      // bit of reflection.
      string retVal = (string)mi.Invoke(this, new object[]{param});

      Console.WriteLine(retVal);
    }

    // Now define the tasks for this task group. As methods are identified 
    // by their attributes it doesn't matter whereabouts in the class the
    // methods are placed.
 
    [Task(ExecuteOrder=2, 
          Identity=(int)TaskIdent.SimpleString,
          Comment="Return unaltered value as text.")]
    public string SimpleString(int value)
    {
      return value.ToString();
    }

    [Task(ExecuteOrder=1, 
          Identity=(int)TaskIdent.SquareString, 
          Comment="Return square of value as text.")]
    public string SquareString(int value)
    {
      value *= value;
      return value.ToString();
    }
  }

Running TaskGroups

TaskGroups are objects with methods and state so you're really only limited by the application's requirements and your own creativity in the ways you can use them.

The example below shows the simplest possible use:

class Program
  {
    static void Main(string[] args)
    {

      TaskGroupExplicitlyOrdered groupExplicit = new TaskGroupExplicitlyOrdered();
      groupExplicit.Execute();

      Console.WriteLine();

      TaskGroupImplicitlyOrdered groupImplicit = new TaskGroupImplicitlyOrdered();
      groupImplicit.Execute();

      Console.WriteLine();

      TaskGroupCustomCall groupCustom = new TaskGroupCustomCall(12);
      groupCustom.Execute();

      Console.ReadLine();
    }
  }

If you wanted to be too clever by half you could create a TaskGroup controller, as shown below, which allows you to not only specify the order within sequences easily but also the order of those sequences.

internal class TaskGroupController : TaskGroup
  {
      [Task(ExecuteOrder=1)]
      public void RunExplicit()
      {
        TaskGroupExplicitlyOrdered groupExplicit = new TaskGroupExplicitlyOrdered();
        groupExplicit.Execute();
      }

      [Task(ExecuteOrder=2)]
      public void RunImplicit()
      {
        TaskGroupImplicitlyOrdered groupImplicit = new TaskGroupImplicitlyOrdered();
        groupImplicit.Execute();
      }

      [Task(ExecuteOrder=3)]
      public void RunCustom()
      {
        TaskGroupCustomCall groupCustom = new TaskGroupCustomCall(12);
        groupCustom.Execute();
      }
  }

...and the code to run umpteen task sequences reduces to...

TaskGroupController controller = new TaskGroupController();
controller.Execute();

The Last Bit

It was pleasant to find how easy, almost trivial, the .NET framework made the implementation of this idea. There is only one method in the three classes with any degree of complexity, findTaskMethods, and even that boils down to not much more than a loop with two or three calls to the System.Reflection library.

All very amusing, you may say, but what does this give you that dropping a series of method calls into a general purpose module doesn't?

  • Simple task identification at run time
  • Strong encapsulation of related processing
  • As there is no need to shunt blocks of code around, it is very much easier to ...
    • ... insert new tasks at any point in the sequence
    • ... change the sequence of tasks
    • ... temporarily exclude tasks from the sequence
  • TaskGroups, being objects can ...
    • ... maintain their own state
    • ... be re-run easily with differing starting state
    • ... be easily handed off to separate threads

History

  • August 2010 - Proof of concept developed
  • November 2010 - Tidied up for publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

cigwork

United Kingdom United Kingdom
Nothing interesting to report.

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberlinuxjr29-Nov-10 3:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 26 Nov 2010
Article Copyright 2010 by cigwork
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid