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:
- Separate data and behavior; currently, your Tasks contain a
DoSomething
method, while they also contain the data they need to execute. - 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.