Search code examples
c#.net-coredllinversion-of-controlwebapi

Dependency Injection inside class library for a net core web api project


I have a .NET Core WEB API project and I would like to be a container for multiples "projects" as .NET CORE Class Library.

What I have is:

  • Solution "Orbit" with a .NET Core Web API project on it.

  • Solution "SpaceRadar" with a .NET Core Class Library.

First, inside my "Orbit" project, the Startup class, what I have made so far:

  public void ConfigureServices(IServiceCollection services)
    {

        this.ConfigureMVC(services);
        this.ConfigureIOC(services);

    }

    private void ConfigureMVC(IServiceCollection services)
    {
        IMvcBuilder mvcBuilder = services.AddMvc(option => { option.EnableEndpointRouting = false; });
        // for each assembly inside modules directory, add the controllers
        foreach (string assemblyPath in Directory.GetFiles($"{System.AppDomain.CurrentDomain.BaseDirectory}/Modules", "*.dll", SearchOption.AllDirectories))
        {
            var assembly = Assembly.LoadFile(assemblyPath);
            mvcBuilder.AddApplicationPart(assembly);
        }
    }

This part works well, since we can trigger the Controller that is defined inside my SpaceRadar project. However I wanted to use dependency injection in my class library project, if possible by scanning the dll to get all the types that extends IScopedServices / ISingletonServices / ITransientServices.

But honestly, I have no idea where to register my interface and their respective implementation. I tried this solution:

private void ConfigureIOC(IServiceCollection services)
    {

        // Store all the type that need to be injected in the IOC system
        List<Type> implementationTypes = new List<Type>();
        List<Type> singletons = new List<Type>();
        List<Type> scopeds = new List<Type>();
        List<Type> transients = new List<Type>();

        // for each assembly, load it, populate the type list of things to be injected 
        foreach (string assemblyPath in Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories))
        {    
            var assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
            implementationTypes.AddRange(assembly.GetExportedTypes().Where(type => type.IsClass && (type.GetInterface("ISingletonServices") != null || type.GetInterface("IScopedServices") != null || type.GetInterface("ITransientServices") != null)));
            singletons.AddRange(assembly.GetExportedTypes().Where(type => type.IsInterface && type.GetInterface("ISingletonServices") != null));
            scopeds.AddRange(assembly.GetExportedTypes().Where(type => type.IsInterface && type.GetInterface("IScopedServices") != null));
            transients.AddRange(assembly.GetExportedTypes().Where(type => type.IsInterface && type.GetInterface("ITransientServices") != null));
        }

        // Register into the service collection
        foreach (Type type in singletons)
        {
            services.AddSingleton(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
        }
        foreach (Type type in scopeds)
        {
            services.AddScoped(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
        }
        foreach (Type type in transients)
        {
            services.AddTransient(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
        }

}

But it seems that there is a problem of context of the assembly. I tried also the Assembly.LoadFrom() but isn't working, the ReaderLoadContext solution found on a blog

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;

namespace Orbit
{
    public class ReaderLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public ReaderLoadContext(string readerLocation)
        {
            _resolver = new AssemblyDependencyResolver(readerLocation);
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }

        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);

            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }

            return IntPtr.Zero;
        }
    }
}

And even to call a "setup" method inside the assembly


        private void ConfigureIOC(IServiceCollection services)
        {
            // for each assembly, load it, add the controller into the mvcBuilder and populate the type list of things to be injected 
            foreach (string assemblyPath in Directory.GetFiles($"{System.AppDomain.CurrentDomain.BaseDirectory}/Modules", "*.dll", SearchOption.AllDirectories))
            {
                Assembly assembly = Assembly.LoadFrom(assemblyPath);
                Type startup = assembly.GetTypes().SingleOrDefault(t => t.Name == "Startup");
                if (startup != null)
                {
                    var setupMethod = startup.GetMethod("Setup");
                    setupMethod.Invoke(setupMethod, new Object[] { services });
                }
            }
        }

and inside my class library

public static void Setup(IServiceCollection services)
        {
            
            List<Type> implementationTypes = new List<Type>();
            List<Type> singletons = new List<Type>();
            List<Type> scopeds = new List<Type>();
            List<Type> transients = new List<Type>();

            foreach (string assemblyPath in Directory.GetFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "*.dll", SearchOption.AllDirectories))
            {
                var assembly = Assembly.Load(assemblyPath);
                // get all Singleton, Scoped and Transient interfaces
                implementationTypes.AddRange(assembly.GetTypes().Where(type => type.IsClass && (type.GetInterface("ISingletonServices") != null || type.GetInterface("IScopedServices") != null || type.GetInterface("ITransientServices") != null)));
                singletons.AddRange(assembly.GetTypes().Where(type => type.IsInterface && type.GetInterface("ISingletonServices") != null));
                scopeds.AddRange(assembly.GetTypes().Where(type => type.IsInterface && type.GetInterface("IScopedServices") != null));
                transients.AddRange(assembly.GetTypes().Where(type => type.IsInterface && type.GetInterface("ITransientServices") != null));
            }

            // Register into the service collection
            foreach (Type type in singletons)
            {
                services.AddSingleton(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
            }
            foreach (Type type in scopeds)
            {
                services.AddScoped(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
            }
            foreach (Type type in transients)
            {
                services.AddTransient(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
            }
            
        }

I also tried the HostingStartup attribute

[assembly: HostingStartup(typeof(SpaceRadar.ServiceKeyInjection))]
namespace SpaceRadar
{
    public class ServiceKeyInjection : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices((context, services) => {
                List<Type> implementationTypes = new List<Type>();
                List<Type> singletons = new List<Type>();
                List<Type> scopeds = new List<Type>();
                List<Type> transients = new List<Type>();

                foreach (string assemblyPath in Directory.GetFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "*.dll", SearchOption.AllDirectories))
                {
                    var assembly = Assembly.Load(assemblyPath);
                    // get all Singleton, Scoped and Transient interfaces
                    implementationTypes.AddRange(assembly.GetTypes().Where(type => type.IsClass && (type.GetInterface("ISingletonServices") != null || type.GetInterface("IScopedServices") != null || type.GetInterface("ITransientServices") != null)));
                    singletons.AddRange(assembly.GetTypes().Where(type => type.IsInterface && type.GetInterface("ISingletonServices") != null));
                    scopeds.AddRange(assembly.GetTypes().Where(type => type.IsInterface && type.GetInterface("IScopedServices") != null));
                    transients.AddRange(assembly.GetTypes().Where(type => type.IsInterface && type.GetInterface("ITransientServices") != null));
                }

                // Register into the service collection
                foreach (Type type in singletons)
                {
                    services.AddSingleton(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
                }
                foreach (Type type in scopeds)
                {
                    services.AddScoped(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
                }
                foreach (Type type in transients)
                {
                    services.AddTransient(type, implementationTypes.Single(t => t.GetInterface(type.FullName) != null));
                }
            });
        }
    }
}

All this solutions doesn't allow me inside my class library to have this kind of controller:

 public class RadarControllers : BaseControllers
    {
        private readonly IRadarServices _radarServices;

        public RadarControllers(IRadarServices radarServices)
        {
            _radarServices = radarServices;
        }

        [HttpGet("radars")]
        public async Task<IActionResult> GetRadars(CancellationToken cancellationToken)
        {
            IList<string> data = await _radarServices.GetRadars(cancellationToken);
            return Ok(data);
        }
    }

How to proceed for this dependency injection ? Thanks.


Solution

  • I finally found a way to deals with that. The issue is that on each projet, the assembly name is the same (for instance : "Services"). .Net core does not like this and it seems that if I try to load through AssemblyLoadContext.Default.LoadFromAssemblyPath, it keep loading the same assembly no matter the path to indicate.

    On the projet, I just right click > Properties and change the assembly name to an another. Thanks to that, AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath) works.

    For instance, if I have project A and B, I rename the assembly names services with A.Services and B.Services

    so The dll is now A.Services.dll instead of Services.dll