When I started developing SolrNet, one of my goals was to have a DI-friendly library but without taking a direct dependency on any specific container. I wanted to enable both IoC and non-IoC users to use the library with equal ease. So, for example, non-IoC consumers would code like this:
var solr = new SolrServer<Document>(ConfigurationManager.AppSettings["solrURL"]);
while IoC users would write (Windsor sample):
container.Register(Component.For<ISolrServer<Document>>() .ImplementedBy<SolrServer<Document>>() .Parameters(Parameter.ForKey("serverURL").Eq(ConfigurationManager.AppSettings["solrURL"])));
and then inject ISolrServer<Document> where they needed it.
But SolrServer itself had a number of dependent components. At first, this wasn't a problem, this number of components was low and component composition was shallow and simple. But as I added features to the library, there were more and more components, interacting in more complex ways. Since I wanted to keep the possibility of writing "new SolrServer...", every component was responsible for instantiating its own dependencies. Soon, I had component instances duplicated everywhere. Dependencies were not clear because I had "shortcut" constructors for each component. In a word, a mess.
It was time to refactor, make dependencies clear and explicit, and introduce a container to manage dependencies. But I didn't want to depend on any specific container! So I took a look at CommonServiceLocator, which is an abstraction over IoC containers. Just what I needed! Except for one little thing: it's a read-only abstraction, that is, it provides an interface to get things out of the container, but no methods to put things in the container. So it's up to the consumer of the library to appropriately register the library's components. This is stated in the docs:
Libraries should not configure the container
As a library or framework author, understand that you should not be putting anything into the container - that's the job of your caller. Allow the application writers to choose whatever container they want. You need to document what services you need registered, and if you're using the ServiceLocation.Current ambient container.
Well... I just don't agree with that. Consumers shouldn't have to know about the library's internal components lifestyles and dependencies. That's why complex stuff with many components are usually packaged into facilities (Windsor) or modules (Ninject, Autofac). But if I used a facility or module I'd be again depending on a single container!
Actually, there are very valid reasons why the CommonServiceLocator doesn't include Register(). Like the name says, it's a locator (read-only) abstraction, not a factory (creation) abstraction. Each container/factory has unique semantics for registering and creating components, different ways to handle lifestyles, disposable components, registering dependencies, and so on. More on this in this discussion between Hammett and Ayende.
But it still wasn't good enough for me, so the only way out was to write my own simple container on top of IServiceLocator. Based on this article from Ayende, I started with the following interface:
public interface IContainer : IServiceLocator { /// <summary> /// Adds a component implementing <typeparamref name="T"/> /// Component key is <typeparamref name="T"/>'s <see cref="Type.FullName"/> /// </summary> /// <typeparam name="T">Service type</typeparam> /// <param name="factory">Component factory method</param> void Register<T>(Converter<IContainer,T> factory); /// <summary> /// Adds a component implementing <typeparamref name="T"/> with the specified key /// </summary> /// <typeparam name="T">Service type</typeparam> /// <param name="factory">Component factory method</param> /// <param name="key">Component key</param> void Register<T>(string key, Converter<IContainer,T> factory); }
Implementing this interface, I could register transient components:
[Test] public void Transient() { var container = new Container(); container.Register<IService>(c => new ServiceImpl()); var inst1 = container.GetInstance<IService>(); var inst2 = container.GetInstance<IService>(); Assert.AreNotSame(inst1, inst2); }
Also singletons:
[Test] public void Singleton() { var container = new Container(); var inst = new ServiceImpl(); container.Register<IService>(c => inst); var inst1 = container.GetInstance<IService>(); var inst2 = container.GetInstance<IService>(); Assert.AreSame(inst, inst1); Assert.AreSame(inst, inst2); }
Being such a simple container, injection is less declarative than in real containers:
[Test] public void Injection() { var container = new Container(); container.Register(c => new AnotherService(c.GetInstance<IService>())); var inst = new ServiceImpl(); container.Register<IService>(c => inst); var svc = container.GetInstance<AnotherService>(); Assert.AreSame(inst, svc.Svc); }
And that's it for the basic container. Later I added component removal for easier testing and I also experimented with per-thread and per-HttpContext lifestyles just for fun, but I didn't really test them. Check out the full tests if you're interested.
Armed with this little container, I was able to clean up my dependencies and hide the internal components to non-IoC consumers while also allowing for other containers to be used, thanks to CommonServiceLocator. More on this when I release the next version of SolrNet.
Source code:
- IContainer
- Container (implementation)
No comments:
Post a Comment