This question is regarding SimpleInjector at the moment, but could apply to any DI Container that may handle this issue a bit better...
I am doing research for my firm right now. I feel a DI Container could help make things way easier for us, but the powers that be have a few issues so I have been set out to try and solve some of them to prove the worth of a DI Container. We have a very large ERP system and we are about to begin our next iteration of our flagship product. This product will contain several large assemblies, probably at least 20-30, all of which, again, will be large.
Our main access point for the user will be a shell, so to speak, that consists of one assembly and as users navigate through the application the other assemblies will be called. We do not want to bog down startup time so we want to find a way to load the assemblies as needed. Only when something from a particular assembly is needed should the assembly load, until then it should not be in memory.
Is there any way to do this with Simple Injector, or and DI Container for that matter? We are writing our application in WPF.
Your requirement is a bit tricky, since the normal use case for DI containers is to load everything up front. With Pure DI this is more trivial, since you simply delay assembly loading, by giving each root type (or group of root types) its own method. Assembly loading happens at the time a method that references types from the assembly is JITted.
So with Pure DI, I image the composition root to look like this:
public static object GetRootType(Type type) {
if (type == typeof(HomeController))
return GetHomeController();
if (type == typeof(ShipOrderController))
return GetShipOrderController();
if (type == typeof(CancelOrderController))
return GetCancelOrderController();
// etc
}
private static object GetCancelOrderController() {
return new CancelOrderController(
ApplyAop(new CancelOrderHandler(new UnitOfWork())));
}
private static object GetShipOrderController() { ... }
private static object GetHomeController() { ... }
private static ICommandHandler<T> ApplyAop<T>(ICommandHandler<T> handler) {
return new TransactionCommandHandlerDecorator<T>(
new ValidationCommandHandlerDecorator<T>(handler));
}
Since the CLR will usually only JIT methods when they are called for the first time, the assembly loading will happen at that point, unless of course such type is referenced before. For instance, if the types that are referenced in the GetRootType
method reference to the assemblies that need to be lazy-loaded, that means that the first hit of GetRootType
will pre-load those assemblies.
Although the use of Pure DI can make it easier to delay assembly loading, when the application is big and contains many classes, the use of a container will typically out-gain Pure DI.
But Simple Injector actually makes this scenario a bit harder than other contains do, because it has a strict lock-down policy, which means that you can't add registrations through the registration API after items are resolved. On the other hand, this lock down policy forces you into a clean model that actually helps preventing numerous hard-to-detect errors.
How to approach this with Simple Injector depends a bit on the application. I would say there are two ways of approaching this. Either you can give each assembly (or group of assemblies) its own container. This allows building up such container when such assembly is loaded. This allows you to define every specific registration that you require in one place, probably centralized in one spot. Downside is that registrations gets more complex if those assemblies aren't isolated; the more types are shared between those assemblies, the harder it gets to get the per-assembly registrations correct.
The other option is to make use of the ResolveUnregisteredType
event that Simple Injector exposes. This allows you to make a last-minute registration when an unregistered type is requested for the first time.
Both options however are still a bit tricky, because you have to make sure not all assemblies are pre-loaded directly at the first call to the container. So the method that runs to check whether a certain assembly is loaded, must itself not reference any types from that assembly and must itself not load the assembly it is looking for. Otherwise you're back to square one.
When using multiple container instances, your solution might look as follows:
var asm1Container = new lazy<Container>(Asm1Bootstrapper.Bootstrap);
var asm2Container = new lazy<Container>(Asm2Bootstrapper.Bootstrap);
var asm3Container = new lazy<Container>(Asm3Bootstrapper.Bootstrap);
mainContainer.ResolveUnregisteredType += (s, e) =>
{
var asmContainer = GetAssemblyContainer(e.UnregisteredServiceType.Assembly);
e.Register(() => asmContainer.GetInstance(e.UnregisteredServiceType));
}
private Container GetAssemblyContainer(Assembly assembly) {
string assemblyName = assembly.FullName;
if (assemblyName.Contains("Assembly1"))
asm1Container.Value;
if (assemblyName.Contains("Assembly2"))
asm2Container.Value;
if (assemblyName.Contains("Assembly3"))
asm3Container.Value;
// etc
}
With the second approach, you would use the ResolveUnregisteredType
event in the same way, but instead allow all registrations to be registered last-minute in the same container.
Last note though. Of course I can't see what kind of "flag ship" application you are building. For most of the applications I've seen in my carrier, this delayed assembly loading doesn't make much sense. It usually only makes sense for really big two-tier applications. Two tier applications are thick-client (Win Forms, WPF, etc) applications that talk directly to a database. Those two-tier application are very rare nowadays, because in most cases adding a third layer (a web service that sits between the client and the database) is much better. Better because the added layer of security you can add, and better because of the extra control you can have at that level (logging, tuning, audit trailing). And at the web service layer, implementing deferred assembly loading has usually little benefit, because web applications can be pre-loaded much easier than it is pre-loading a thick client applications.
This of course doesn't work for all types of applications. For instance if you're building something like Visual Studio (which uses Managed Extensibility Framework for delayed assembly loading btw), adding a third tier is very often a very good approach. And the overhead of the third tier is really small when you're building your application around patterns like these and these.