Runtime: Load assemblies with nuget package dependencies

Created on 12 Sep 2016  路  18Comments  路  Source: dotnet/runtime

I have been trying to dynamically load assemblies that have dependencies on nuget packages, of course the nuget packages have been restored and the proper dlls are present in the output folder.

I do this to load the assembly:
var aName = AssemblyLoadContext.GetAssemblyName(file.FullName);
var library = Assembly.Load(aName);
And then I load the types like this:
types = library.GetTypes();
When the assembly does not uses any dependency, the types are loaded correctly, but when the assembly uses a nuget package I get an exception with a loader exception like this:
Message = "Could not load file or assembly 'needletail_dotnet_core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified."

I can dynamically load the assemblies that are part of the nuget packages and get their types.

So my question is how to load assemblies that have nuget packages(or any other) depenency on dotnet core?

I am using dotnet version 1.0.1 Build cee57bf6c981237d80aa1631cfe83cb9ba329f12.

Some suggested answers on the internet use beta versions of dotnet core.

area-System.Runtime documentation needs more info

Most helpful comment

+1 @rdghickman, any example that solve complex dynamic loading a library with complex dependencies?

All 18 comments

I opened this as an issue because of the lack of documentation, in the end I solved my problem the ugly way, it works, but is not a solution that can be applied to every case.

I think that the lack of official documentation related with reflection is something that should be taken seriously, most of the examples or solutions found in the internet are effective only for beta versions and most of those solutions are not even from official channels.

I am confused what is lacking from your point of view -- is it end-to-end example or some specific API description? (The 3 APIs you listed above should give you what you need, or is it missing assembly resolution API?)
What might help to clarify: Post a sample that you think is missing. Link the API docs you looked at which you believe should have the sample / place where you would expect the docs.

I definitely feel an end-to-end example is sorely lacking. I have been fumbling around for days and have not yet reached a solution that appears clean and safe. I am assuming there is _some_ kind of solution but I have not yet been able to determine what it is.

The first project I have builds a netcoreapp1.1 with an entry point. This is the host that will dynamically load a DLL. The other project is a netstandard1.6 library that builds the DLL that will be hosted. Both projects depend upon a _third_ project which is an API library which contains shared interfaces.

Firstly, if the host directly depends upon the library explicitly at compile time, then the publish and run works, as expected.

In dotnet core 1.0 the following worked in the trivial case:

```c#
private sealed class PluginAssemblyLoadContext : AssemblyLoadContext
{
///
protected override Assembly Load(AssemblyName assemblyName)
=> LoadFromAssemblyPath(assemblyName.Name);
}

```c#
var pluginAssembly = new PluginAssemblyLoadContext().LoadFromAssemblyPath(
                Path.Combine(assemblyPath, $"plugin-{pluginName}.dll"));

This stopped working in dotnet 1.1, as it would fail to find System.Runtime. The implementation was changed to this:

```c#
private sealed class PluginAssemblyLoadContext : AssemblyLoadContext
{
///
protected override Assembly Load(AssemblyName assemblyName)
=> Assembly.Load(assemblyName);
}

At this point, the dynamic load operation again succeeds for the trivial case.

Now, imagine the dynamically loaded library has a dependency upon something else. This case would fail so I again changed it to this rather clumsy implementation:

```c#
        private class DirectoryLoader : AssemblyLoadContext
         {
             private readonly DirectoryInfo _path;

             public DirectoryLoader(DirectoryInfo path)
             {
                 _path = path;
             }

             protected override Assembly Load(AssemblyName assemblyName)
             {
                 try {
                     return Default.LoadFromAssemblyName(assemblyName);
                 } catch (Exception) {
                     return LoadFromAssemblyPath(Path.Combine(_path.FullName, assemblyName.Name + ".dll"));
                 }
             }
         }

Effectively it would try and load from the default context and if that fails try and load it from the custom directory. This fixes the issue where it loads a DLL from the custom directory but that depends upon something like a core runtime library.

Now take the scenario where the library has a dependency upon a library with platform-specific versions, such as System.Diagnostics.TraceSource. When you dotnet publish this, you get multiple DLLs all in separate directory trees which the custom loader will not find because it is only looking in a specific directory and not subdirectories. One cannot include all files from all subdirectories because the same file exists multiple times. It also seems very unsafe to try and guess the directory structure than dotnet publish will use.

I may be being stupid here, but I am not yet sure of what the correct approach to solving this problem is.

+1 @rdghickman, any example that solve complex dynamic loading a library with complex dependencies?

We have a scenario in which at startup assemblies are scanned using a search pattern and loaded dynamically. The following code worked up to a certain point:

AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFullPath)

In some scenarios it might happen that the assembly we are trying to dynamically load is also a referenced assembly that end users are referencing to use some classes/interfaces. In which case the above code miserably fails with a FileNotFoundException (and no inner details).

We fixed it by using the following approach, that seems to take into account all our scenarios:

static class AssemblyLoader
{
    public static Assembly Load(string assemblyFullPath)
    {
        var fileNameWithOutExtension = Path.GetFileNameWithoutExtension(assemblyFullPath);

        var inCompileLibraries= DependencyContext.Default.CompileLibraries.Any(l => l.Name.Equals(fileNameWithOutExtension, StringComparison.OrdinalIgnoreCase));
        var inRuntimeLibraries = DependencyContext.Default.RuntimeLibraries.Any(l => l.Name.Equals(fileNameWithOutExtension, StringComparison.OrdinalIgnoreCase));

        var assembly = (inCompileLibraries || inRuntimeLibraries)
            ? Assembly.Load(new AssemblyName(fileNameWithOutExtension))
            : AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFullPath);

        return assembly;
    }
}

But I'm not sure, so I'm interested in more guidance/documentation as well.

I am sorry for the late response, what I would like to see is an official example and also a more detailed documentation, as for "more detailed documentation" I mean something like the documentation from .Net Framework where you can navigate to old versions of the framework, there are many examples in the same page and links to related classes, methods, etc.

My original problem was not only to dynamically load assemblies with dependencies, but assemblies that have dependencies on nuget packages, in .Net core when you build a web application, in the folder where your app is built you will not see all the dlls required by your application, you only see those dlls when you publish your app, in .Net framework all the dlls are copied to the bin folder, so any dependency can be loaded since the files are in the same folder.

I got a similar problem where I dynamically load an assembly and accessing it's types.
Once I create an instance of that type it throws a FileNotFound exception about a third party nuget.
In my case my library depends on RabbitMQ.Client.

I also don't understand why RabbitMQ.Client.dll is not present in the build output directory of my library?

Making sure your third party dlls exist in the same folder as your dynamically loaded dll seems to fix this.
By default the dependencies are not compiled with your dll so I had to add

<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

to my csproj file. Sadly it outputs the entire framework dependencies as well.

I'm still having trouble with exactly the same scenario that @pedro-ramirez-suarez initially described. I use the same code.

My (executing) ASP.NET Core MVC assembly (assembly A) will load (at runtime) an assembly (assembly B) that references a nuget package (package C). A does NEITHER reference B NOR C.
I (dotnet) publish assembly A and assembly B and copy the contents of assembly B's bin-folder (including package C's assemblies) to assembly A's bin-folder, since there ist no reference from A to B that would cause B to reside there implicitely.

If I now run A (dotnet assemblyA.dll) the app is running and receiving and handling requests that require B - as long as those methods do NOT use package C. If they do, I receive the mentioned FileNotFoundException for package C although it physically is present in the folder.

After 5 months in this issue, is there still not real solution to this problem? Having A reference B in csproj makes everything work, but the abstraction-value would be lost then...

I use dotnet version 2.0.0 (also tried 2.0.3).

What I found out:
If I manually modify the output A.deps.json file and I add package C to "targets" and "libraries", it works! Those entries can be found in B.deps.json to copy and paste.
But what does that even mean? I don't want A to know C...
Also, it isn't recommended to meddle with deps.json as I unterstand.

The issue I linked here is related, I was under the impression that using --additional-deps would 'merge' the deps and thus work. This is however not the case, and I'm not sure if it should be. @natemcmaster since you've documented some of this, would you be able to give us some insight or pointers to documentation?

using @mauroservienti 's solution

public static class AssemblyLoader
    {
        public static Assembly LoadFromAssemblyPath(string assemblyFullPath)
        {
            var fileNameWithOutExtension = Path.GetFileNameWithoutExtension(assemblyFullPath);
            var fileName = Path.GetFileName(assemblyFullPath);
            var directory = Path.GetDirectoryName(assemblyFullPath);

            var inCompileLibraries = DependencyContext.Default.CompileLibraries.Any(l => l.Name.Equals(fileNameWithOutExtension, StringComparison.OrdinalIgnoreCase));
            var inRuntimeLibraries = DependencyContext.Default.RuntimeLibraries.Any(l => l.Name.Equals(fileNameWithOutExtension, StringComparison.OrdinalIgnoreCase));

            var assembly = (inCompileLibraries || inRuntimeLibraries)
                ? Assembly.Load(new AssemblyName(fileNameWithOutExtension))
                : AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyFullPath);

            if(assembly != null)
            LoadReferencedAssemblies(assembly,fileName, directory);

            return assembly;
        }

        private static void LoadReferencedAssemblies(Assembly assembly,string fileName,string directory)
        {
            var filesInDirectory = Directory.GetFiles(directory).Where(x=>x!= fileName).Select(x=>Path.GetFileNameWithoutExtension(x)).ToList();
            var references = assembly.GetReferencedAssemblies();

            foreach(var reference in references)
            {
                if (filesInDirectory.Contains(reference.Name))
                {
                    var loadFileName = reference.Name+".dll";
                    var path = Path.Combine(directory, loadFileName);
                    var loadedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
                    if(loadedAssembly != null)
                    LoadReferencedAssemblies(loadedAssembly, loadFileName,directory);
                }
            }

        }

    }

This worked for my scenario, references are loaded if they are in the same directory as the assembly that references them

@hvanbakel their isn't really good API yet in .NET Core for loading assemblies dynamically from a NuGet cache. You can implement some of this on your own using AssemblyLoadContext and DependencyContext, but I haven't seen this work 100% well everywhere. To get this right, you have manage a lot of complexity. The best docs I've seen on what .NET Core does to resolve assemblies is here: https://github.com/dotnet/core-setup/blob/master/Documentation/design-docs/corehost.md and https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/assemblyloadcontext.md

By the way, I've been asked about this subject a lot over the last year. I decided to write up some working examples of how to use AssemblyLoadContext and Microsoft.Extensions.DependencyModel together to create a loader which imitates most of the behavior of corehost. See https://github.com/natemcmaster/DotNetCorePlugins for details.

@natemcmaster you are a hero man. I'm going to replace my code gathered across all these topics with your plugin loader.

AssemblyLoadContext issues are not Reflection issues - they should be labeled `area-System.Runtime.Loader'

I have similar issue. I developed a library and it's required to be loaded dynamically to my main project. I publish it to nuget and reference it to my project but it doesn't load unless I have to manually copy the dll to the output folder.

image
image

I had to load multiple assemblies and all its dependend assemblies (local and nuget) in a custom context because each assembly could have references to an assembly with different versions.

I first tried to load each assembly by using DependencyContext.Load(myassembly) but this returned always null which drove me crazy :-).

So I figured out that DependencyContext.Load(myassembly) only returns a context when you load .net core assemblies which have a deps.json-file with the same name as the used assembly in the same directory. All dependend (including none .net core) will resolved by using the Resolving-Method.

my.example.dll
my.example.deps.json

Here is my code which worked for me (I used this [Link] codeproject and made my modifications to it.).
It finds local dependencies and nuget assemblies.

using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

namespace my.name.space
{
    public sealed class AssemblyResolver : IDisposable
    {
        private readonly ICompilationAssemblyResolver _assemblyResolver;
        private readonly DependencyContext _dependencyContext;
        private readonly AssemblyLoadContext _loadContext;
        public Assembly InitialAssembly { get; }

        public AssemblyResolver(string assemblyPath)
        {
            //this assemblyPath has to have a deps.json-file in the same directory.
            InitialAssembly = Assembly.LoadFile(assemblyPath);

            _dependencyContext = DependencyContext.Load(InitialAssembly);

            var resolver = new ICompilationAssemblyResolver[]
            {
                new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(assemblyPath)),
                new ReferenceAssemblyPathResolver(),
                new PackageCompilationAssemblyResolver()
            };

            _assemblyResolver = new CompositeCompilationAssemblyResolver(resolver);

            _loadContext = AssemblyLoadContext.GetLoadContext(InitialAssembly);
            _loadContext.Resolving += OnResolving;
        }

        /// <summary>
        /// returns all assemblies of the current context.
        /// </summary>
        /// <returns></returns>
        public List<Assembly> GetAssemblies()
        {
            return _loadContext.Assemblies.ToList();
        }

        /// <summary>
        /// Resolving dependencies of the assembly
        /// </summary>
        /// <param name="context"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name)
        {
            bool NamesMatch(RuntimeLibrary runtime)
            {
                var res = string.Equals(runtime.Name, name.Name, StringComparison.OrdinalIgnoreCase);
                if(!res)
                {
                    //iterate through every assemblygroup. This will recognize also assemblies in nuget-packages.
                    foreach(var group in runtime.RuntimeAssemblyGroups)
                    {
                        foreach(var l in group.RuntimeFiles)
                        {
                            if (Path.GetFileNameWithoutExtension(l.Path) == name.Name) //optional version-check:  && l.AssemblyVersion == name.Version.ToString())  
                                return true;
                        }
                    }
                }

                return res;
            }

            RuntimeLibrary library = _dependencyContext.RuntimeLibraries.FirstOrDefault(NamesMatch);
            if (library == null)
                return null;

            var wrapper = new CompilationLibrary(
                                library.Type,
                                library.Name,
                                library.Version,
                                library.Hash,
                                library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths),
                                library.Dependencies,
                                library.Serviceable
                            );

            var assemblies = new List<string>();
            _assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies);
            var assembly = assemblies.FirstOrDefault(a => Path.GetFileNameWithoutExtension(a) == name.Name);

            return assembly==null
                    ? null
                    : _loadContext.LoadFromAssemblyPath(assembly);

        }

        public void Dispose()
        {
            _loadContext.Resolving -= this.OnResolving;
        }
    }
}

I am getting the same issue with Quartz NuGet package. Can anyone help me to resolve it ?

Was this page helpful?
0 / 5 - 0 ratings