How to load assemblies located in a folder in .NET Core console app
Asked Answered
T

8

46

I'm making a console app on the .NET Core platform and was wondering, how does one load assemblies (.dll files) and instantiate classes using C# dynamic features? It seems so much different than .NET 4.X and it's not really documented...

For example, let's say I have a class library (.NET Core) and it has only one class:

namespace MyClassLib.SampleClasses
{
    public class Sample
    {
        public string SayHello(string name)
        {
            return $"Hello {name}";
        }

        public DateTime SayDateTime()
        {
            return DateTime.Now;
        }
    }
}

So the name of the dll file would be MyClassLib.dll and it's located in /dlls/MyClassLib.dll.

Now I want to load this in a simple console app (.NET Core) and instantiate the Sample class and call the methods using dynamic features of C# in the following console app:

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // load the assembly and use the classes
        }
    }
}

Note: By .NET Core, I mean the RC2 version.

Triphthong answered 18/6, 2016 at 9:13 Comment(0)
T
19

Not sure if it's the best way to do it but here's what I came up with:

(Only tested on .Net Core RC2 - Windows)

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var asl = new AssemblyLoader();
            var asm = asl.LoadFromAssemblyPath(@"C:\Location\Of\" + "SampleClassLib.dll");

            var type = asm.GetType("MyClassLib.SampleClasses.Sample");
            dynamic obj = Activator.CreateInstance(type);
            Console.WriteLine(obj.SayHello("John Doe"));
        }

        public class AssemblyLoader : AssemblyLoadContext
        {
            // Not exactly sure about this
            protected override Assembly Load(AssemblyName assemblyName)
            {
                var deps = DependencyContext.Default;
                var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
                var assembly = Assembly.Load(new AssemblyName(res.First().Name));
                return assembly;
            }
        }
    }
}

MyClassLib.SampleClasses is the namespace and Sample is the type aka class name.

When executed, it will try to load the SampleClassLib.dll compiled class library in the memory and gives my console app access to MyClassLib.SampleClasses.Sample (take a look at the question) then my app calls the method SayHello() and passes "John Doe" as name to it, Therefore the program prints:

"Hello John Doe"

Quick note: The override for the method Load doesn't matter so you might as well just replace its content with throw new NotImplementedException() and it shouldn't affect anything we care about.

Triphthong answered 18/6, 2016 at 10:55 Comment(5)
AssemblyLoadContext is not available in asp.net core 1.0, it can be found only in System.Runtime.Loader 4.0.0-beta-23516Brisket
@DilyanDimitrov I use the above code on version 1.0 with no problem.Triphthong
In which package you can find this AssemblyLoadContext class, mine VS suggests to add the package I mentioned aboveBrisket
@DilyanDimitrov Here's the whole abstract class gist.github.com/VSG24/9fe807a1f96073e2d4b2705a117e7439 As I said, if you just want to load some assembly, just return null in the Load methodTriphthong
Old question but here is official MSDN link for this question: "Create a .NET Core application with plugins" learn.microsoft.com/en-us/dotnet/core/tutorials/…Amphoteric
A
45

Currently running against netcoreapp1.0 you don't actually need to go to the extent of implementing your own AssemblyLoader. There is a Default that exists which works just fine. (Hence @VSG24 mentioning that the Load doesn't do anything).

using System;
using System.Runtime.Loader;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\MyDirectory\bin\Custom.Thing.dll");
            var myType = myAssembly.GetType("Custom.Thing.SampleClass");
            var myInstance = Activator.CreateInstance(myType);
        }
    }   
}

with project.json looking like:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },

  "dependencies": {
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.1"
    },
    "System.Runtime.Loader": "4.0.0"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}
Aila answered 10/11, 2016 at 6:37 Comment(1)
Doesn't load .NET Core assemblies, it surprisingly loads normal .NET Framework assemblies only!!!Caldron
T
19

Not sure if it's the best way to do it but here's what I came up with:

(Only tested on .Net Core RC2 - Windows)

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;

namespace AssemblyLoadingDynamic
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var asl = new AssemblyLoader();
            var asm = asl.LoadFromAssemblyPath(@"C:\Location\Of\" + "SampleClassLib.dll");

            var type = asm.GetType("MyClassLib.SampleClasses.Sample");
            dynamic obj = Activator.CreateInstance(type);
            Console.WriteLine(obj.SayHello("John Doe"));
        }

        public class AssemblyLoader : AssemblyLoadContext
        {
            // Not exactly sure about this
            protected override Assembly Load(AssemblyName assemblyName)
            {
                var deps = DependencyContext.Default;
                var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
                var assembly = Assembly.Load(new AssemblyName(res.First().Name));
                return assembly;
            }
        }
    }
}

MyClassLib.SampleClasses is the namespace and Sample is the type aka class name.

When executed, it will try to load the SampleClassLib.dll compiled class library in the memory and gives my console app access to MyClassLib.SampleClasses.Sample (take a look at the question) then my app calls the method SayHello() and passes "John Doe" as name to it, Therefore the program prints:

"Hello John Doe"

Quick note: The override for the method Load doesn't matter so you might as well just replace its content with throw new NotImplementedException() and it shouldn't affect anything we care about.

Triphthong answered 18/6, 2016 at 10:55 Comment(5)
AssemblyLoadContext is not available in asp.net core 1.0, it can be found only in System.Runtime.Loader 4.0.0-beta-23516Brisket
@DilyanDimitrov I use the above code on version 1.0 with no problem.Triphthong
In which package you can find this AssemblyLoadContext class, mine VS suggests to add the package I mentioned aboveBrisket
@DilyanDimitrov Here's the whole abstract class gist.github.com/VSG24/9fe807a1f96073e2d4b2705a117e7439 As I said, if you just want to load some assembly, just return null in the Load methodTriphthong
Old question but here is official MSDN link for this question: "Create a .NET Core application with plugins" learn.microsoft.com/en-us/dotnet/core/tutorials/…Amphoteric
H
12

Thanks for your sharing. It is working with Net Core 1.0 also. If your assembly needs another assemblies at the same path, you can use the code sample below.

using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
public class AssemblyLoader : AssemblyLoadContext
{
    private string folderPath;

    public AssemblyLoader(string folderPath)
    {
        this.folderPath = folderPath;
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        var deps = DependencyContext.Default;
        var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
        if (res.Count > 0)
        {
            return Assembly.Load(new AssemblyName(res.First().Name));
        }
        else
        {
            var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll");
            if (File.Exists(apiApplicationFileInfo.FullName))
            {
                var asl = new AssemblyLoader(apiApplicationFileInfo.DirectoryName);
                return asl.LoadFromAssemblyPath(apiApplicationFileInfo.FullName);
            }
        }
        return Assembly.Load(assemblyName);
    }
}

Remember to add the following dependencies to your project.json file:

 "System.Runtime.Loader"
 "Microsoft.Extensions.DependencyModel"
Heraclitus answered 9/8, 2016 at 6:27 Comment(2)
And if it needs another Assembly that's over in the .NET package directories?Inscription
This works for loading an assembly, but it doesn't seem fully correct, checking IsSubclassOf of the underlying types in the newly loaded types return false for known inherited types..Withdraw
R
7

Using .NET Core 1.1 / Standard 1.6, I found that AssemblyLoader was not available, and AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath) gave me a "Could not load file or assembly xxx" error.

Finally, this solution below worked for me - purely by adding a step to get the AssemblyName object:

var assemblyName = AssemblyLoadContext.GetAssemblyName(assemblyPath);
var assembly = Assembly.Load(assemblyName);
Rivarivage answered 26/7, 2017 at 11:2 Comment(2)
Thanks! Unfortunately, I also have AssemblyLoadContext.Default.Resolving that also raises the exception.Ripuarian
The first solution now works in .NET Core 2.x and later.Heidt
P
4

@Rob, the only way I could get your example to build was to change your "myInstance" type to dynamic.

Leaving the type as var does allow the code to build but as soon as I try and use a method from the runtime loaded assembly, I get compiler errors such as myInstance does not contain method X. I'm new at this but marking the type as dynamic, does seem to make sense. If the type is loaded at runtime then how can the compiler verify myInstance will contain method X or prop Y ? By typing the myInstance as dynamic I believe you are removing the compiler checking and thus I could get the example to build and run just fine. Not sure this is 100% the correct way ( I don't know enough and you may advise that there's an issue using dynamic?) but it is the only way I got it to work without having to go to the trouble of creating my own AssemblyLoader (as you correctly point out).

So...

using System;
using System.Runtime.Loader;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\Documents\Visual Studio 2017\Projects\Foo\Foo\bin\Debug\netcoreapp2.0\Foo.dll");
            var myType = myAssembly.GetType("Foo.FooClass");
            dynamic myInstance = Activator.CreateInstance(myType);
            myInstance.UpperName("test");
        }
    }
}

Hope this helps someone as being new it took me ages to pinpoint why myInstance as a var didn't have method X etc Doh!

Proton answered 20/12, 2017 at 17:50 Comment(1)
This is correct. Type inference doesn't usually work when dealing with dynamic types.Triphthong
P
1

I dig a lot into that, I tried the DependencyContext approach... It works well but it has some limitations and it is different from the standard assembly resolution that is in the the c++ dotnet app that starts your app. You have to do name matching manually and if your host app is a published one, you won't have the probing path for the nuget folder which is a problem (solveable) if your child assembly is in debug and uses nuget...

So here is another solution: If the app (assemblyA) manually loading an assembly (assemblyB) has no dependencies (or no conflicting dependencies with assemblyB) I suggest cheating and defaulting to the assembly resolution of assemblyB. There is an hidden gem for dotnet.exe that enable you to load the deps file of your choice so you can do something like this:

dotnet exec --depsfile pathToAssemblyB\assemblyB.deps.json --runtimeconfig pathToAssemblyB\assemblyB.runtimeconfig.json AssemblyA.dll

and then you can load the assembly as explained in other answers with

var myAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath("pathToAssemblyB\\AssemblyB.dll");

This way it will correctly resolve all dependencies for assemblyB but won't for assemblyA. It is a reverse problem but if you have a small app that want to do some remoting in a big app, it is useful. Another problem is that you need to know that you are going to use assemblyB when starting your app and that it works only once per execution. So there is a different set of problems and you can choose your approach depending on your situation. Please note that it is an unsupported/undocumented feature but it is used in EF core tooling, so it is "viable" for now...

Phraseogram answered 13/11, 2018 at 14:10 Comment(0)
A
0

I think this below will work for you, and hope this can help some MEF2 newbie like me.

    /// <summary>
    /// Gets the assemblies that belong to the application .exe subfolder.
    /// </summary>
    /// <returns>A list of assemblies.</returns>
    private static IEnumerable<Assembly> GetAssemblies()
    {
        string executableLocation = AppContext.BaseDirectory;
        string directoryToSearch = Path.Combine(Path.GetDirectoryName(executableLocation), "Plugins");
        foreach (string file in Directory.EnumerateFiles(directoryToSearch, "*.dll"))
        {
            Assembly assembly = null;
            try
            {
                //Load assembly using byte array
                byte[] rawAssembly = File.ReadAllBytes(file);
                assembly = Assembly.Load(rawAssembly);
            }
            catch (Exception)
            {
            }

            if (assembly != null)
            {
                yield return assembly;
            }
        }
    }

another one, but in .netstandard1.3, neither was available.

var assembiles = Directory.GetFiles(Assembly.GetEntryAssembly().Location, "*.dll", SearchOption.TopDirectoryOnly)
        .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath);
Avelar answered 9/10, 2019 at 9:57 Comment(0)
M
0

I've been using the following code for loading an assembly, and invoke a method inside a class from the loaded assembly.

    private static FormCustomized loadLayout(global::System.String layoutFilename, global::System.String layoutNameSpace)
    {
        FormCustomized mainForm = default;
        Type typeMainLayout = default;
        FileInfo layoutFile;
        layoutFile = new FileInfo(layoutFilename);
        layoutFile.Refresh();
        if (!layoutFile.Exists)
        {
            MessageBox.Show("Layout file not found. You need to reinstall the program");
            return default;
        }

        try
        {
            Assembly assemblyRaw = Assembly.LoadFrom(layoutFilename);
            AssemblyLoadContext context = AssemblyLoadContext.Default;
            Assembly assembly = context.LoadFromAssemblyPath(layoutFilename);


            Type typeMainLayoutIni = assembly.GetType(layoutNameSpace + ".initializeLayoutClass");
            Object iniClass = Activator.CreateInstance(typeMainLayoutIni, true);
            MethodInfo methodInfo = typeMainLayoutIni.GetMethod("AssembliesToLoadAtStart");
            enVars.assemblies = (Dictionary<string, Environment.environmentAssembliesClass>)methodInfo.Invoke(iniClass, default);
            typeMainLayout = assembly.GetType(layoutNameSpace + ".mainAppLayoutForm");
            mainForm = Activator.CreateInstance(typeMainLayout, enVars) as FormCustomized;
        }
        catch (Exception ex)
        {
            return default;
        }

        return default;
    }
Mullin answered 12/8, 2020 at 13:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.