Skip to content

Re-architectured App Bootstrapping + Configuring remote host by hostname instead of IP Address

TL;DR

This re-architecture opens up some possibilities to allow for plugins and connectors to rebind certain dependencies, allowing the main app to provide implementations of certain interfaces that a module can then override. The plus side is that this change also makes it a little less ambiguous when certain bindings are set up.

Use-Case

The main use-case that I used as motivation was to enable configuring a host address using a domain address. It would still require you as a driver to set up Dynamic DNS, but ultimately makes it simple for a race engineer to correctly configure a host when not on the same network and you don't have to provide an updated IP Address if your ISP had decided to change your public-facing IP Address.

image

The Details

Ninject provides a whole host of tools to allow the loading of plugins at run-time. I've leveraged this to try and make the loading process a little more explicit. After reaching out to you via email you sounded like you'd welcome a contribution that cleans things up a little and I hope that this is to your liking.

The old process

From what I understood, the previous process was as follows:

  1. On startup, load all DLLs in the main directory not already loaded into memory.
  2. load the DLLs in the "plugins" directory into memory.
  3. Scan the assemblies for types that implement the ISecondMonitorPlugin interface.
  4. Instantiate instances using Activator.
  5. Load the DLLs in the "Connectors" directory into memory.
  6. Scan the assemblies for types that implement the IGameConnector interface.
  7. Instantiate instances using the Activator

Behind the scenes, the default constructor for each of these instances instantiated by the Activator would query the KernelWrapper for their dependencies. If the KernelWrapper doesn't have a binding for the dependency, it will scan all loaded assemblies for instances implementing the INinjectModuleBootstrapper interface, instantiate that and call the GetModules method to get the various instances of objects inheriting from the NinjectModule type. This would then be loaded into a singleton StandardKernel that was maintained by the KernelWrapper.

It means that implicitly new modules will get loaded as plugins and connectors are loaded into memory, but it's hidden deep beneath a complex process that's calling into the KernalWrapper singleton. This now also puts application dependency bootstrapping at the centre of the application's architecture and allows for later calls to the KernalWrapper singleton instance to resolve a dependency, only to load whatever new modules are available. The side effect of this is that a plugin/connector can't provide a rebinding of a specific interface, because the call to get a dependency from the KernelWrapper won't find a fault that would warrant loading the relevant dependencies into the kernel.

The new process

Due to the fact that there are multiple executable applications, each having slightly different needs with regards to application bootstrapping, I've pulled this responsibility out of the core of the architecture and hoisted it to the outer layer. The introduction of the SecondMonitor.Bootstrapping project introduces a few extension methods that enable a consumer to load dynamic modules as it sees fit.

This changes the process to the following:

  1. On App startup, construct a StandardKernal instance.
  2. Use the LoadCoreAssemblies extension method to load app DLLs as assemblies and scans for any types with a default constructor inheriting from NinjectModule which then gets loaded into the kernel.
  3. Use the LoadPlugins to load plugin DLLs into memory and load the relevant NinjectModules into the kernel.*
  4. Use the LoadConnectors to load connector DLLs into memory and load the relevant NinjectModules into the kernel.*
  5. Use the kernel.Get<>() method to resolve the application's execution root and run the execution root.

Steps 3 and 4 also take care of loading any NinjectModule types found in the newly loaded DLLs as well as creating a convention-based binding for the plugin/connector interface.

In my opinion, this now allows me to more easily reason about application bootstrapping and it still allows for plugins & connectors to be built that can stand completely on their own without needing to add dependencies like Ninject or other projects within the application. It's also removing weird binding errors where modules aren't loaded because it's relying on binding failures, but rather making it a more explicit part of application bootstrapping.

AssemblyLoaderCollector

I'd like to make a quick special note on a type I created. It's nothing fancy, but it allowed me to use AppDomain.Current.Load() in such a way that I only get the assemblies that haven't already been loaded. This is important because the kernel.Load(assemblies) method would throw an exception if an assembly has a NinjectModule that has already been loaded into the kernel. It's a small little helper type that made it easier than having to query to see if an assembly had already been loaded.

Removed

This allowed me to get rid of the following types:

  • KernelWrapper
  • BootstrapHelper
  • INinjectModuleBootstrapper and all of its implementations

Conclusion

There are still one or two architectural quirks that have the potential to get in my way for a future contribution, but I feel that this already makes things a little clearer. Truth be told, if this contribution does get accepted and released it will already make life much easier when trying to have a remote race engineer. I feel that I've learned a lot from going through this exercise and that it is at least paving the way towards realising a seamless experience where the remote application can switch between active servers without any input required from the race engineer. I do hope that these changes won't have too much of an impact on current feature work you're doing, but I don't believe I changed the core of what you've done and rather just found a way to enhance it a little. :)

Edited by Sas van der Westhuizen

Merge request reports