0
votes

We're using Ninject dependency injection for a machine automation software and have run into a problem with circular references. We have several physical objects that needs to be aware of each other to avoid collisions. We have added a CollisionHandler class that authorizes each movement. For example: The robot arm checks that the hatch is open before moving through it. And the hatch checks that the robot arm is out of the way before closing.

I performed a proof of concept test of the Ninject setter method injection pattern with just the robot and the collision handler. That worked fine, first the Robot was created, then the CollisionHandler and finally the inject method was called on the Robot. Both Robot, Hatch and CollisionHandler are bound as singletons:

Bind<Robot>().ToSelf().InSingletonScope()
Bind<Hatch>().ToSelf().InSingletonScope()
Bind<CollisionHandler>().ToSelf().InSingletonScope()

public class Robot
{
    private CollisionHandler _collisionHandler;

    public Robot(ISomeService someService)
    {
    }

    [Inject]
    public void PostConstructInject(CollisionHandler collisionHandler)
    {
        if (_collisionHandler != null)
            throw new InvalidOperationException("PostConstructInject called more than once");
        _collisionHandler = collisionHandler;
    }
}

public class CollisionHandler
{
    private readonly Robot _robot;
    private readonly Hatch _hatch;

    public CollisionHandler(Robot robot, Hatch hatch)
    {
        _robot = robot;
        _hatch = hatch;
    }

    public bool IsRobotAwayFromHatch() { }
    public bool IsHatchOpen() { }
}

It all seemed fine so I moved on to implementing the pattern for the other entities. That's where it stopped working. Adding the inject method to the hatch, Ninject is no longer able to construct the object graph:

public class Hatch
{
    private CollisionHandler _collisionHandler;

    public Hatch(ISomeOtherService someOtherService)
    {
    }

    [Inject]
    public void PostConstructInject(CollisionHandler collisionHandler)
    {
        if (_collisionHandler != null)
            throw new InvalidOperationException("PostConstructInject called more than once");
        _collisionHandler = collisionHandler;
    }
}

The problem is that Ninject wants to call the inject method directly after constructing the object:

Activation path:
4) Injection of dependency CollisionHandler into parameter collisionHandler of method PostConstructInjection of type Robot
3) Injection of dependency Robot into parameter robot of constructor of type CollisionHandler
2) Injection of dependency CollisionHandler into parameter collisionHandler of method PostConstructInjection of type Hatch
1) Request for Hatch

This equals the following code:

Robot robot = new Robot(someService);
robot.PostConstructInject(/* We need a CollisionHandler instance before it is constructed */);
Hatch hatch = new Hatch(someOtherService);
CollisionHandler collisionHandler = new CollisionHandler(robot, hatch);
hatch.PostConstructInject(collisionHandler);

What I would like it to do is to move the PostConstructInject calls to after the CollisionHandler instance has been created:

Robot robot = new Robot(someService);
Hatch hatch = new Hatch(someOtherService);
CollisionHandler collisionHandler = new CollisionHandler(robot, hatch);
robot.PostConstructInject(collisionHandler);
hatch.PostConstructInject(collisionHandler);

Is there some way I can tell Ninject to hold off calling the injection method until it is possible? I could remove the [Inject] attribute and have the CollisionHandler call those methods but that feels quite ugly.

1
I think you need an approach where something is orchestrating the whole process instead of allowing the individual components to drive the system. The Hatch and the Arm should not care about each other, they have specific things they do and it's up to the orchestrator (probably the robot class) to handle this. This is good because it keeps your components simple and robust and allows responsibilities to be separated.The Muffin Man

1 Answers

1
votes

There are ways to do this. But in general if you have such cyclic dependencies then you are on the wrong path with your design. There is nearly no good reason to have cyclic dependencies. They make many things much more complicated not just building up your instances. You will easily end up with a completely tangled design that will get hard to maintain.

If you really want to follow this path despite my warning than have a look at the factory extension and support for Lazy. I won't give you more hints as I really think this is not a good design at all.