It is not a secret that ASP.NET core comes with dependency injection support out of the box. And we don’t remember ever feeling it lacks features. All we have to do is register a type in Startup.cs
and it’s ready to be consumed in our controllers:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<IDBLogger, IdbLogger>();
}
}
//
public class HomeController : Controller
{
private readonly IDBLogger _idbLogger;
public HomeController(IDBLogger idbLogger)
{
_idbLogger = idbLogger; // all good here!
}
...
}
What if it’s a plugin?
Now imagine we’ve got a Order
type that we for whatever strange reason load at runtime dynamically.
public class Order
{
private readonly IDBLogger _logger; // suppose we've got the reference from common assembly that both our main application and this plugin are allowed to reference
public Order(IDBLogger logger)
{
_logger = logger; // will it resolve?
}
public void GetOrderDetail()
{
_logger.Log("Inside GetOrderDetail"); // do we get a NRE here?
}
}
Load it in the controller
External assembly being external kind of implies that we want to load it at the very last moment – right in our controller where we presumably need it. If we try explore this avenue, we immediately see the issue:
public HomeController(IDBLogger idbLogger)
{
_idbLogger = idbLogger;
var assembly = Assembly.LoadFrom(Path.Combine("..\Controllers\bin\Debug\netcoreapp3.1", "Orders.dll"));
var orderType = assembly.ExportedTypes.First(t => t.Name == "Order");
var order = Activator.CreateInstance(orderType); //throws System.MissingMethodException: 'No parameterless constructor defined for type 'Orders.Order'.'
orderType.InvokeMember("GetOrderDetail", BindingFlags.Public | BindingFlags.Instance|BindingFlags.InvokeMethod, null, order, new object[] { });
}
The exception makes perfect sense – we need to inject dependencies! Making it so:
public HomeController(IDBLogger idbLogger)
{
_idbLogger = idbLogger;
var assembly = Assembly.LoadFrom(Path.Combine("..\Controllers\bin\Debug\netcoreapp3.1", "Orders.dll"));
var orderType = assembly.ExportedTypes.First(t => t.Name == "Order");
var order = Activator.CreateInstance(orderType, new object[] { _idbLogger }); // we happen to know what the constructor is expecting
orderType.InvokeMember("GetOrderDetail", BindingFlags.Public | BindingFlags.Instance|BindingFlags.InvokeMethod, null, order, new object[] { });
}
Victory! or is it?
The above exercise is nothing new of exceptional – the point we are making here is – dependency injection frameworks were invented so we don’t have to do this manually. In this case it was pretty easy but more compex constructors can many dependencies. What’s worse – we may not be able to guarantee we even know all dependencies we need. If only there was a way to register dynamic types with system DI container…
Yes we can
The most naive solution would be to load our assembly on Startup.cs
and register needed types along with our own:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddScoped();
// load assembly and register with DI
var assembly = Assembly.LoadFrom(Path.Combine("..\\Controllers\\bin\\Debug\\netcoreapp3.1", "Orders.dll"));
var orderType = assembly.ExportedTypes.First(t => t.Name == "Order");
services.AddScoped(orderType); // this is where we would make our type known to the DI container
var loadedTypesCache = new LoadedTypesCache(); // this step is optional - i chose to leverage the same DI mechanism to avoid having to load assembly in my controller for type definition.
loadedTypesCache.LoadedTypes.Add("order", orderType);
services.AddSingleton(loadedTypesCache); // singleton seems like a good fit here
}
And that’s it – literally no difference where the type is coming from! In controller, we’d inject IServiceProvider
and ask to hand us an instance of type we cached earlier:
public HomeController(IServiceProvider serviceProvider, LoadedTypesCache cache)
{
var order = serviceProvider.GetService(cache.LoadedTypes["order"]); // leveraging that same loaded type cache to avoid having to load assembly again
// following two lines are just to call the method
var m = cache.LoadedTypes["order"].GetMethod("GetOrderDetail", BindingFlags.Public | BindingFlags.Instance);
m.Invoke(order, new object[] { });
// Victory!
}