Code Buckets

Buckets of code

.Net

Dynamically Loading Assemblies for Dependency Injection in .Net Core

We want to get all our assemblies registered for Dependency Injection in a .Net Core 3 application by scanning the bin folder and registering all the classes which implement a specific interface. Bit of a mouthful so a simple (and classic) example is

public class Foo: IFoo
{
}

public interface IFoo : ILifecycle
{
}
public class Bar: IBar
{
}

public interface IBar: ILifecycle
{
}
public class Bar: IBar
{
}

public interface IBar: ILifecycle
{
}

We could register each object by its own interface i.e.

services.AddTransient<IFoo, Foo>();
services.AddTransient<IBar, Bar>();

That’s fine for 2 but for hundreds it will be a pain. We want to register the assemblies based on an interface that they all inherit – in this case ILifecycle. Also, we want to register all the assemblies in a given folder, typically the bin folder. This is what didn’t work in .Net Core for me and hence the post – no answer on the internet (that I could find) so I’m filling the internet gap.

Implementation

We will use Scrutor as our DI framework. It’s an extension of the native .Net DI framework with extensions for scanning and registering assemblies en mass.

Incorrect Implementation – LoadFromFile

Using an MVC application and within Startup

public void ConfigureServices(IServiceCollection services)
{
//..lots of other code

foreach (string assemblyPath in Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories))
{
  var assembly = Assembly.LoadFile(assemblyPath);
  assemblies.Add(assembly);
}

//.. register
services.Scan(scan => scan
   .FromAssemblies(assemblies)
   .AddClasses(classes => classes.AssignableTo<ILifecycle>(), publicOnly: false)
   .AsImplementedInterfaces()
   .WithTransientLifetime());

//.. test
var provider = services.BuildServiceProvider();
var foo = provider.GetService<IFoo>(); //.. returns null
var bar = provider.GetService<IBar>(); //.. return null
}

Frustratingly this doesn’t work and it’s not obvious why. The test returns null Foo and Bar objects even though we have registered them in the correct way i.e. by scanning for ILifecycle and registering the object as their matching interface. I we debug and look at the loaded assemblies everything looks fine. The assemblies have loaded and I can use reflection to poke into them further and satisfy myself that they are indeed the correct assemblies. Except they aren’t

Correct Implementation – AssemblyLoadContext

public void ConfigureServices(IServiceCollection services)
{
//..lots of other code

foreach (string assemblyPath in Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories))
{
var assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
assemblies.Add(assembly);
}

//.. register
services.Scan(scan => scan
.FromAssemblies(assemblies)
.AddClasses(classes => classes.AssignableTo<ILifecycle>(), publicOnly: false)
.AsImplementedInterfaces()
.WithTransientLifetime());

//.. test
var provider = services.BuildServiceProvider();
var foo = provider.GetService<IFoo>(); //.. returns null
var bar = provider.GetService<IBar>(); //.. return null
}

Very similar except we have swapped

Assembly.LoadFile(assemblyPath);

for

System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);

Now it works and the DI framework can provide us with the correct objects based on their matching interface. All is good.

Explanation

I don’t really have one so if anyone can post a comment explain this then that would be very interesting. I do know that .Net Core handles its dependencies differently to .net framework i.e. it’s all NuGet packages so it makes sense that we need another way to correctly load the assemblies with their dependencies. What’s puzzling is that Assembly.LoadFromFile is still there and appears to work – except it doesn’t for DI.

Other DI Frameworks

I wasn’t exhaustive but I tried the with StructureMap and AutoFac and got the same issue. So, it’s not limited to Scrutor. You’d face these problems with other frameworks as well.

Useful Links

Stack Overflow question on How to load assemblies located in a folder in .net core console app. It’s where I found the proper way to load assemblies

Git Hub for Scutor. Works well (when you get over the assembly loading issue).

7 COMMENTS

  1. Not sure what your references are but I get the following error message
    ‘ServiceCollection’ does not contain a definition for ‘Scan’ and no accessible extension method ‘Scan’ accepting a first argument of type ‘ServiceCollection’ could be found

  2. Before this one, I was using this method myself, but I don’t know if there is a big difference in performance.

    Assembly.Load(“xxx”)
    .GetTypes()
    .Where(w => w.Namespace == “xxx.Repository” && w.IsClass)
    .ToList()
    .ForEach(t => {
    services.AddTransient(t.GetInterface(“I” + t.Name, false), t);
    });

  3. Hey,
    I just wanted to say thank you!
    I was struggling with .NET Core DI not working when trying to achieve dynamic config loading.
    Invocation of the dynamically resolved method with body:
    “`services.Configure(serviceXName, configurationSection);“`
    just did not actually register anything.
    I just replaced one line with my DLL loader helper following your suggestion and boom – I just worked!
    Thanks again!

  4. I prefer your approach as it doesn’t depend on external libraries. Saved me because I’m using Net 6.0 and Scrutor only supports up to Net 5.0

LEAVE A RESPONSE

Your email address will not be published. Required fields are marked *