Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Castle Windsor registation unexpected behavior

I've noticed a strange behavior:

public class Bar : IBar
{
...something here
}

public class Foo : IBar
{
...something here
}
container.Register(Component.For<IBar>().ImplementedBy<Bar>());
var test1 = container.Resolve<IBar>(); //returns Bar
container.Register(Component.For<Foo>().ImplementedBy<Foo>());
var test2 = container.Resolve<IBar>(); //returns Foo

Why test2 is Foo? I hadn't registered Foo as IBar, I've clearly registered it as implementation for Foo. I though only Bar should be resolved, as it's the only implementation forwaded for IBar. The project isn't mine. There could be some strange settings made by other devs.

like image 519
Danil Eroshenko Avatar asked Apr 15 '19 10:04

Danil Eroshenko


1 Answers

The very short answer is that if executed in isolation, the code shown above would work as you expect it to, returning Bar not Foo. If it is returning Foo then there are other component registrations made with the same container.

In this scenario a unit test is an easy way to see whether something behaves the way we think it should in isolation.

This test passes:

[TestMethod]
public void registration_returns_expected_type()
{
    var container = new WindsorContainer();
    container.Register(Component.For<IBar>().ImplementedBy<Bar>());
    var test1 = container.Resolve<IBar>(); //returns Bar
    container.Register(Component.For<Foo>().ImplementedBy<Foo>());
    var test2 = container.Resolve<IBar>();
    Assert.IsInstanceOfType(test2, typeof(Bar));
}

Based on that I'd look to see what else is registering dependencies. If you change the above so that both Foo and Bar are registered as implementations of IBar, it runs and still returns Bar.

If we change the Foo registration to this:

container.Register(Component.For<IBar>().ImplementedBy<Foo>().IsDefault());

...then IBar is resolved as Foo.

What can also happen is that there are multiple installers (classes that implement IWindsorInstaller) in the project to keep registrations small and manageable, but they contain conflicting registrations. They get executed something like this:

container.Install(FromAssembly.This());

...or using a command that executes installers from other assemblies.

In that scenario, the registration that "wins" at startup can be non-deterministic. I've been bitten by that where someone else added a different registration for the same component, my code still worked fine, but in another environment the other component was selected.

In most real-world cases when we design interfaces, we know whether it's intended to be used with single or multiple implementations at runtime. If it's a simple application and we don't think that's an issue then this might not be a concern.

If we're planning up front for multiple implementations - perhaps we intend to resolve a collection of something - then we might handle this up front with named dependencies.

container.Register(Component.For<IBar>().ImplementedBy<Bar>().IsDefault());

Now if someone else registers a different implementation they'll need to use named dependencies or some other mechanism to resolve theirs.

But there's still a problem. Nothing prevents them from specifying IsDefault when registering their dependencies, and then you're back to square one again.

(You'll see that everything after this goes down a rabbit hole of taking additional steps to defend against what might never happen.)

A reasonable approach is that if you register an interface implementation, if you think it's possible that another implementation is registered (and if it's easy to find out) then you could check. If there isn't another, register yours as the default. Then the next person, seeing your default, will avoid conflicting with it. That's hardly foolproof.

One defense is to use named dependencies yourself, although that's inconvenient if you don't need them. (And the underlying problem here is that it's not perfectly obvious whether you need them or not if various installers are used.)

If you're creating a separate installer for a few components it's likely that you've got specific implementations in mind, so you can specify them.

public class MyInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Component.For<NeedsBar>().DependsOn(Dependency.OnComponent<IBar, Foo>()));
    }
}

I'd rather not have to do that just to prevent a conflict that might never occur.

This leaves a few options, all of which are flawed:

  • Be aware of the potential issue and watch out for it. I don't like "be careful" as a solution for anything, but after getting impacted by this once I've found that it's been enough.
  • Run integration tests that configure your container using all of your runtime components and then inspect the container for duplicate registrations which are both default or non-default. Even knowing the potential for duplicate registrations I've never felt the need to do this.
  • Hopefully if an interface is so general that it's used in components application-wide then you can either avoid multiple implementations, or in that case the awareness of how it's used leads to unambiguous registrations. In other cases, just define separate interfaces closer to where they're needed.
like image 103
Scott Hannen Avatar answered Oct 29 '22 18:10

Scott Hannen