3
votes

For a long time now we've been fortunate enough to have the Common Service Locator (CSL) to resolve services from an unknown source. However, there has never been any container-agnostic solution for registering these services in the first place. We've always been faced with the problem of having to couple our composition code to a particular IoC container, and furthermore, be committed to using that container throughout our entire application.

I feel I may be trying to achieve the impossible here, but does anyone have any ideas on how to achieve a Common Service Registry (CSR)?

My original idea was to use MEF to resolve various IContainerIntegrators (one class for each container technology), which in turn use MEF to resolve various IXXXContainerBindings (one interface for each technology). Users could then develop applications around container bindings, and just drop their bindings into the BIN directory to achieve a plugin architecture. If they wanted to use a new container technology, then they would just have to develop a new IContainerIntegrator class and accompanying IXXXContainerBinding, which they would then use to write their own bindings. At application startup the CSR aggregates all of the container instances into a single CSL using a ServiceLocatorAggregator class.

I have got this working, but am faced with the following problems:

  • Any binding which makes calls to the current (incomplete) container is unstable, since the previous registrations may be overridden by subsequent bindings. This includes bindings which need to resolve objects to make registration decisions (i.e. 'bind this if that is present').
  • Two bindings may exist which expose the same set of services. However, we want to 'interlace' these registrations. E.g. 'I want services A and C from binding X, and service B and D from binding Y'.
  • Containers resolve services from themselves when auto-wiring dependencies of requested services. E.g. 'container.Bind<This>.To<That>()' will automatically inject the implementation with services resolved from 'container' - not from the aggregated service locator.

Please shout at me if I've completely missed the point here, but isn't this the most decoupled solution for dependency management? Wouldn't it be nice? I'm about to embark on a big enterprise project with a plugin architecture. I don't want to commit to a particular IoC container.

(p.s. This question is about container-agnostic composition which supports discovery. Please do not enter a debate on SL vs DI. SL is used in composition, which is why I've referenced it so much here).

2
I think it makes sense not to be dependent on one specific IoC container. But using several of them at the same time? That sounds like a bad idea to me.svick
What other options do we have? I don't think we can have both. The Application Vendor may use Autofac, but Plugin Vender A may use Ninject, and Plugin Vendor B may use Castle.Lawrence Wagerfield
I think that what the plugin uses internally shouldn't matter, as long as your plugin API provides all that is necessary directly.svick
SO answer Dependency Inject (DI) “friendly” library may be useful to you.bentayloruk
@svick - Not sure if I understand your point. Bindings (or plugins) may swap-out existing implementations with new ones. E.g. Our application may depend on a ISearchProvider. The default binding may register with LuceneProvider, but later down the line a new binding is released which registers GoogleProvider. What I'm saying is that these two bindings shouldn't have to use the same container technology... but currently they do.Lawrence Wagerfield

2 Answers

9
votes

The most decoupled (and the best) solution you can achieve is to realize that loose coupling is best achieved through principles and patterns instead of particular technologies.

Use Constructor Injection throughout your application. The ensures that none of your application layers need reference any container at all. Then compose the entire application graph in the root of the application.

You don't need to use a DI Container for this, but if you choose to use a DI Container, you must isolate it to the Composition Root. This means that if you later decide to migrate to a different container, you only need to change the Composition Root. However, Poor Man's DI is also an option.

1
votes

There are a number of tricks you can use to avoid having to depend on a specific IoC container throughout most of your code, the simplest of which is to use constructor injection. If you're dead-set on using a Service Locator pattern, just create your own Service Locator class, which wraps the actual IoC container kernel you're planning to use.

That said, the point of an IoC container is to achieve "Inversion of Control": i.e. to move the control from the bottom levels to the top. This means that you need to have a point near the "top" (or "root") of your application that is actually aware of all the service implementations it's going to use, as well as your specific IoC implementation. This should be restricted to a handful of classes. Generally the "Context Root" of an application is the place where you will initialize your IoC container and Service Locator. There should be a specific module or group of modules that takes care of setting up all your bindings.

If you want to allow for plugins, you need to create a specific API for them to use and conform to. Simply allowing other packages to define new IoC bindings willy-nilly is a recipe for disaster, since you don't know how well these different packages will play together.

ASP.NET MVC 3 is a good example of this. They have specific service factory locators that you override within the Global Application_Start method. In order to implement one of these factories, you have to abide by the API that they provide you. But you can create an implementation that uses any IoC container you want, or none at all. You're not changing "bindings" at all. You're just telling the framework that for the current application, you want it to use "this factory" to create controllers or model metadata providers instead of using the default factory.

To use another example that's more applicable to your specific example, let's take the case of an ISearchProvider. You might have a built-in LuceneProvider, and maybe one of your plugins can provide a GoogleProvider. Which of these providers do you want to use? Does the mere presence of the GoogleProviderPlugin mean that the LuceneProvider is no longer available? Should searches somehow combine the results of both of these providers? Should the user be able to choose one or more providers from within the user interface?

Regardless of the answer to these questions, the ultimate point is that you want your application to control this, not the plugin. Rather than giving the plugin carte blanche to muck with your DI bindings, you want to tell the plugin, "I am allowing you to define additional search providers, and here is how you can register them." They can be registered by a variety of means, including class annotations/attributes or the mere presence of a class that implements a given interface. But the important point is that there is an API that specifically defines what they can "plug in" to, and what you require of anyone who builds a plugin.

Now, if the GoogleProvider has dependencies that are defined within the plugin, that plugin can resolve those dependencies however it wants. Hopefully it will use some kind of IoC container, but if it doesn't, that's no skin off your back. You can still be agnostic as to the kind of container they use, if any.

If there are certain services that you expect a SearchProvider to require, you can either include those services, or factories for those services, as part of the initialization API for your plugin. That way your plugin can access those services without needing to be aware of your application's IoC container.