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).
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
Install-Package Scrutor -Version 3.2.1 fixed my issues, Sorry for not investigating more.
No worries. Glad it’s working for you. 🙂
Hello,
I try to register class library but the method System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
is not working on the class library. It seems that is a context issue. Do you have any idea ?
https://stackoverflow.com/questions/63843313/dependency-injection-inside-class-library-for-a-net-core-web-api-project
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);
});
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!
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