DI Framework Challenges, 2: Lists
June 26th, 2008
There is another feature that I do not find in the dependency injection frameworks I’ve seen — list injection.
For example, let’s suppose you have a kind of a notification service that sends messages using all possible delivery providers. It makes sense to have a list of these in the service:
public class NotificationService { private readonly IDeliveryProvider[] providers; public NotificationService(IDeliveryProvider[] providers) { this.providers = providers; } public void SendAll(INotification[] notifications) { var deliveries = from notification in notifications from provider in providers from delivery in provider.GetDeliveries(notification) select delivery; this.DeliverAll(deliveries); } }
Now the problem is that there is no out-of-the-box support for such things in Castle, which is somewhat unexpected.
Yes, I can set arrays through configuration. But I never use XML configuration unless there is an extremely important reason for this to be configurable at deployment time, which does not happen very often with IoC containers. Also, documented approach implies that I have to hardcode a list of specific services here, which makes it a change preventer.
So configuration-only solution is out of the question.
Fortunately, in contrast with previous challenges, it is possible to implement this myself. There is a minor problem — Castle feature for getting an array of services (kernel.ResolveServices) is generic-only in RC3. But if I have to choose between hackarounds and using nameless trunk version, I prefer trunk version.
There is a naive version of the resolver:
internal class ListSubDependencyResolver : ISubDependencyResolver { private static readonly HashSet<Type> SupportedInterfaces = new HashSet<Type> { typeof(IEnumerable<>), typeof(ICollection<>), typeof(IList<>) }; private readonly IKernel kernel; public ListSubDependencyResolver(IKernel kernel) { this.kernel = kernel; } public bool CanResolve(CreationContext context, ISubDependencyResolver parentResolver, ComponentModel model, DependencyModel dependency) { if (parentResolver.CanResolve(context, parentResolver, model, dependency)) return true; var targetType = dependency.TargetType; if (targetType.IsArray) return true;
return targetType.IsInterface
&& targetType.IsGenericType
&& SupportedInterfaces.Contains(targetType.GetGenericTypeDefinition());
} public object Resolve(CreationContext context, ISubDependencyResolver parentResolver, ComponentModel model, DependencyModel dependency) { if (parentResolver.CanResolve(context, parentResolver, model, dependency)) return parentResolver.Resolve(context, parentResolver, model, dependency); var type = ( from @interface in dependency.TargetType.GetInterfaces() where @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>) select @interface.GetGenericArguments().First() ).Single(); return this.kernel.ResolveAll(type, new Hashtable()); } }
Side note: here we can see two minor problems with API design — first one is the mysterious parentResolver (what should I pass as parentResolver to parentResolver?), second one is that shiny new ResolveAll has no overload for the case when I do not need additionalArguments.
This is a very naive implementation (no checks if I can actually resolve generic parameter), but it works quite well with the original case (as well as with the other IList/ICollection/IEnumerable requirements).
However, there is a logical consistency problem with Singletons — they will get different parameters depending on whether you have resolved them before registering all their dependencies or after. If a Singleton has such dependencies, it can not be Startable, unless you make sure to register all dependencies before it.
I have found almost no information on using lists with DI, so I am not sure how others solve this. But I do not need Startable and all my components are registered at almost same time, so this is not a problem for me for now.
Update: Thanks to Victor Kornov, I have found a this post by Castle developer, which describes the same solution. I have added a proposition to the corresponding Google Group to make it a built-in feature. I prefer it being enabled by default, but I would like to see it either way.