Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dynamically load assemblies in dotnet core

I'm building a web application, where I would like separate concerns, i.e. having abstractions and implementations in different projects.

To achieve this, I've tried to implement a composition root concept, where all implementation must have an instance of ICompositionRootComposer to register services, types etc.

public interface ICompositionRootComposer
{
    void Compose(ICompositionConfigurator configurator);
}

In projects that are referred directly in the build hierarchy, implementations of ICompositionRootComposer are called, and services are registered correct in the underlying IoC container.

The problem arises when I'm trying to register services in a project, where I've set up a post build task that copies the built dll to the web project's debug folder:

cp -R $(TargetDir)"assembly and symbol name"* $(SolutionDir)src/"webproject path"/bin/Debug/netcoreapp1.1

I'm loading the assembly with: (Inspiration: How to load assemblies located in a folder in .net core console app)

internal class AssemblyLoader : AssemblyLoadContext
{
    private string folderPath;

    internal AssemblyLoader(string folderPath)
    {
        this.folderPath = Path.GetDirectoryName(folderPath);
    }

    internal Assembly Load(string filePath)
    {
        FileInfo fileInfo = new FileInfo(filePath);
        AssemblyName assemblyName = new AssemblyName(fileInfo.Name.Replace(fileInfo.Extension, string.Empty));

        return this.Load(assemblyName);
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        var dependencyContext = DependencyContext.Default;
        var ressource = dependencyContext.CompileLibraries.FirstOrDefault(r => r.Name.Contains(assemblyName.Name));

        if(ressource != null)
        {
            return Assembly.Load(new AssemblyName(ressource.Name));
        }

        var fileInfo = this.LoadFileInfo(assemblyName.Name);
        if(File.Exists(fileInfo.FullName))
        {
            Assembly assembly = null;
            if(this.TryGetAssemblyFromAssemblyName(assemblyName, out assembly))
            {
                return assembly;
            }
            return this.LoadFromAssemblyPath(fileInfo.FullName);
        }

        return Assembly.Load(assemblyName);
    }

    private FileInfo LoadFileInfo(string assemblyName)
    {
        string fullPath = Path.Combine(this.folderPath, $"{assemblyName}.dll");

        return new FileInfo(fullPath);
    }

    private bool TryGetAssemblyFromAssemblyName(AssemblyName assemblyName, out Assembly assembly)
    {
        try
        {
            assembly = Default.LoadFromAssemblyName(assemblyName);
            return true;
        }
        catch
        {
            assembly = null;
            return false;
        }
    }
}

With this I'm able to load the assembly and call the projects ICompositionRootComposer implementation.

But the problem is that it doesn't seem to recognize any of my types.

When calling my configurator with

configurator.RegisterTransiantService<IFoo, Foo>();

it should register IFoo and Foo in the IoC.
But when debugging I'm not able to get info of the types, i.e via typeof(Foo) in the debug console in Visual Studio Code.

like image 570
ComkeenDk Avatar asked Apr 11 '17 18:04

ComkeenDk


3 Answers

Are you aware that ASP.NET Core has it's own, built-in Dependency Injection mechanism? It can be easily switched to other IoC container for your needs, I don't think that you need to reinvent it.

What you need to do here is use a reflection to make a generic method and call after that, something like this:

public void ConfigureServices(IServiceCollection services)
{
    var myAssembly = LoadAssembly();
    // find first interface
    var firstInterfaceType = myAssembly.DefinedTypes.FirstOrDefault(t => t.IsInterface).GetType();
    // find it's implementation
    var firstInterfaceImplementationType = myAssembly.DefinedTypes.Where(t => t.ImplementedInterfaces.Contains(firstInterfaceType)).GetType();

    // get general method info
    MethodInfo method = typeof(IServiceCollection).GetMethod("AddTransient");
    // provide types to generic method
    MethodInfo generic = method.MakeGenericMethod(firstInterfaceType, firstInterfaceImplementationType);
    // register your types
    generic.Invoke(services, null);
}
like image 80
VMAtm Avatar answered Oct 26 '22 17:10

VMAtm


Necromancing.


You can create a wrapper class for the old Assembly.LoadFile to do that.

This has the added benefit that you can stay backward-compatible with dotnet-none-core by applying search-and-replace changes in old code-bases.

namespace System.Reflection
{
    public class Assembly2
    {
        public static System.Reflection.Assembly LoadFile(string path)
        {
            System.Reflection.Assembly assembly = null;
            
#if NET_CORE
            // Requires nuget - System.Runtime.Loader
            assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
#else
            assembly = System.Reflection. Assembly.LoadFile(path);
#endif 
            return assembly;
        }
    }
}

You'll need to add System.Runtime.Loader via NuGet.

like image 26
Stefan Steiger Avatar answered Oct 26 '22 15:10

Stefan Steiger


I found a solution to my problem.

It turned out, that I had the property PreserveCompilationContext set to true, and that's why the debugger wouldn't register my manually copied assembly. When I removed the property from the web project csproj file, everything worked.

like image 20
ComkeenDk Avatar answered Oct 26 '22 16:10

ComkeenDk