Question

I'm creating a background task controller as following:

public class TaskController
{
    private TaskBase task;
    public TaskController(ITask task)
    {
        this.task = task;
    }

    public void DoSomething()
    {
        task.DoSomething();
    }
}

ITask interface:

interface ITask
{
    void DoSomething();
}

TaskBase abtract class:

public abtract class TaskBase : ITask
{
    \\some common fields/properties/methods   

    public void DoSomething()
    {
        \\perform action here
    }
}

Task implementation:

public class Task1 : TaskBase
{
    public Task1(string arg, int arg1)
    {
    }        
}

public class Task2 : TaskBase
{
    public Task2(bool arg, double arg)
    {
    }
}

This is an example on how to use it:

public void DoTask(string arg, int arg1)
{
    Task1 task = new Task1(arg, arg1);
    TaskController controller = new TaskController(task);
    controller.DoSomething();
}

As you can see, I'm using manual injection in this approach. Now I want to switch to using IoC like NInject but after done some research, there's 2 things still bug me.

1. How can I tell the binding which concrete task to use in particular context?
2. How to pass dynamic arguments (`arg` and `arg1` on above example) to `Bind<T>` method

Note: Please leave some comment if you see my question deserve a downvote in order to help me avoid making mistake in the future

Was it helpful?

Solution

The problems you have are caused by your design. If you change your design, the problems will go away. There are a few things you should do:

  1. Separate data and behavior; currently, your Tasks contain a DoSomething method, while they also contain the data they need to execute.
  2. And related, inject runtime data into your components' constructors.

If you extract the data from the behavior, you'll get the following:

// Definition of the data of Task1
public class Task1Data
{
    public string Arg;
    public int Arg1;
}

// The behavior of Task1
public class Task1 : ITask<Task1Data> {
    public void Handle(TTask1Data data) {
        // here the behavior of this task.
    }
}

Here every task implements the generic ITask<TTaskData> interface:

public interface ITask<TTaskData>
{
    Handle(TTaskData data);
}

With this design in place, we can now use it as follows:

private ITask<Task1Data> task1;

public Consumer(ITask<Task1Data> task1) {
    this.task1 = task1;
}

public void DoTask(string arg, int arg1)
{
    task1.Handle(new Task1Data { Arg = arg, Arg1 = arg1 });
}

And we register our tasks as follows:

kernel.Bind<ITask<Task1Data>>().To<Task1>();
kernel.Bind<ITask<Task2Data>>().To<Task2>();
kernel.Bind<ITask<Task3Data>>().To<Task3>();

Although I'm not very experienced with Ninject, I'm sure there's a way to transform these registrations to a convenient single line.

This design has many advantages. For instance, it makes adding cross-cutting concerns much easier. For instance, you can create a generic decorator that wraps each task in a transaction as follows:

public class TransactionTaskDecorator<T> : ITask<T> {
    private readonly ITask<T> decoratee;
    public TransactionTaskDecorator(ITask<T> decoratee) {
        this.decoratee = decoratee;
    }

    public void Handle(T data) {
        using (var scope = new TransactionScope()) {
            this.decoratee.Handle(data);
            scope.Complete();
        }
    }
}

Such decorator can be applied without the consumer having to know anything about it, since it just depends on the ITask<T> interface.

You can also add a decorator that allows executing the tasks in a background thread:

public class BackgroundTaskDecorator<T> : ITask<T> {
    private readonly Func<ITask<T>> decorateeFactory;
    private readonly ILogger logger;
    public TransactionTaskDecorator(Func<ITask<T>> decorateeFactory, ILogger logger) {
        this.decorateeFactory = decorateeFactory;
        this.logger = logger;
    }

    public void Handle(T data) {
        Task.Factory.StartNew(() =>
        {
            try {
                // We're running on a different thread, so we must create the task here.
                var decoratee = this.decorateeFactory.Invoke();
                decoratee.Handle(data);
            } catch (Exception ex) {
                this.logger.Log(ex);
            }
        }
    }
}

You can learn more about this design here.

OTHER TIPS

1) Use Named properties like so

 public TaskController([Named("MyName")] ITask task)

Then in the NinjectModule

Bind<ITask>().To<Task1>().Named("MyName");

2) Think you can use the same method as above

https://github.com/ninject/ninject/wiki/Contextual-Binding

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top