Linking .NET Assemblies with IoC
August 25, 2014 4 Comments
Suppose you like Inversion of Control (IoC). I do. And suppose you like to encapsulate shared assemblies so the internal details are just that– internal. I like that, too. But those two likes can conflict when a consuming application needs to register all the dependencies in an IoC container: those internal types are going to be invisible to the container.
Well, I want what I want when I want it. So here are my demands…
- An application– MyApplication– using an IoC container to fill dependencies via constructor-based injection
- A shared assembly– SharedAssembly– filling its own dependencies however it well pleases (it’s none of my business)
- The shared assembly exposing just a few interfaces– not classes– to work with, save one factory. No implementations; no visible internals/guts;
So NONE of this nonsense:
namespace MyApplication ... var myClass = new SharedAssembly.MyClass();
Instead, MyApplication classes will depend on only the public interfaces defined in SharedAssembly.
My friend Drew once created a policy where each shared assembly came with a basic factory to create instances of public interfaces. I liked how that gave consumers the ability to work with just the interfaces and thereby respected the boundaries of the assemblies. I’ll use that same idea here and create a factory in SharedAssembly to return instances of those public interfaces.
The IoC container in MyApplication will use the factory from SharedAssembly to obtain instantiated implementations of SharedAssembly’s public interfaces. The IoC container will then inject those implementations into the dependent classes in MyApplication.
The Code
In SharedAssembly, I’ll create a basic factory that accepts a type and returns an object…
public static class Factory { public static Object Resolve( Type type ) { return type == typeof( IBusinessEntity ) ? new BusinessEntity() : null; } }
I could design it to be more “manual” (e.g., CreateBusinessEntity() ). Also, here is where SharedAssembly could use its own IoC container of whatever type it wants, or none at all. This factory is a static class. If an instance would be better, do that. Let the details of the situation be your guide.
Now, over in MyApplication, I’ll reference SharedAssembly. And, in the IoC container initialization, I’ll register IBusinessEntity with a call to Factory.Resolve( IBusinessEntity ). For now, I’m going to use StructureMap 3.1.0.133, mostly because it’s an awesome IoC container, but don’t get bogged down in religious or political battles here. Use whichever one you want; just set it up properly so you can easily switch it out. If the container is getting passed around or you see more than just a couple references to it in an application, you’ve already been hit. Get help now.
Taking some pointers from code I’ve read, I like to bury all the IoC registration in a folder/namespace called DependencyResolution. In there, I have this class…
public class IoC { public static IContainer Initialize() { var container = new Container( config => { config.AddRegistry<DefaultRegistry>(); config.AddRegistry<SharedAssemblyRegistry>(); } ); return container; } }
StructureMap allows us to define multiple registries, so I’ve defined one for MyApplication– DefaultRegistry– and one for SharedAssembly– SharedAssemblyRegistry…
public class SharedAssemblyRegistry : Registry { public SharedAssemblyRegistry() { Scan(scanner => { scanner.Assembly( "SharedAssembly" ); scanner.With( new CustomConvention() ); }); } }
Additionally, StructureMap allows us to use custom-defined conventions when scanning an assembly. During a scan, the convention is called for each public type discovered (not internals, etc.). It accepts the Type and the Registry and decides how to register that type in that registry. Here, I’ve defined CustomConvention which for every type discovered except Factory, registers it against the Factory.Resolve() method…
public class CustomConvention : IRegistrationConvention { public void Process( Type type, Registry registry ) { if ( type != typeof( Factory ) ) registry.For( type ).Use( "SharedAssembly types", ctx => Factory.Resolve( type ) ); } }
So, when the IoC container in MyApplication attempts to resolve some class called MyBusinessObject, which declares a dependency on IBusinessEntity, the IoC container itself will first call Factory.Resolve(), and then use the returned object to construct MyBusinessObject.
The Theory
Don’t let the auto-scanning coolness of StructureMap distract you– the solution was really the factory. This hinge point gives SharedAssembly control of instantiating its internals, which means it can use dependency injection if desired, or not. SharedAssembly can avoid using an IoC container today and easily add one when complexity warrants it. SharedAssembly can declare an ILog or IRepo used by internal classes and then provide a consumer (e.g., MyApplication) with the ability to submit an implementation (e.g., Factory.Register(ILog, MyAppLog) ). And if SharedAssembly depends on other assemblies that have their own factories (do you have a policy?), here is where they can be linked, too. The point is, you can do whatever you want! So start dreaming and open your mind to new possibilities.