2

I'm currently using AssemblyLoadContext.Default.LoadFromAssemblyPath(path/to/netstandard1.6lib.dll) and was curious about how to handle any nuget dependencies that library may have?

For example: Library A dynamically loads Library B. Library B depends on Redis from NuGet.

Library B loads correctly, but upon using the redis client -- we get a nasty FileNotFoundException complaining that the redis assembly cannot be found. The scenario is really a typical module-loader type thing.

Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
if (assembly == null)
    throw new InvalidExtensionException(name, path);

TypeInfo type = assembly.DefinedTypes.FirstOrDefault(x => x.ImplementedInterfaces.Contains(typeof(IExtension)));
if (type == null)
    throw new InvalidExtensionException(name, path);

IExtension extension = Activator.CreateInstance(type.AsType(), name, _dependencyUtility) as IExtension;
if (extension == null)
    throw new InvalidExtensionException(name, path);

extensions.Add(extension);

When Activator creates the instance, the extension's constructor attempts to make a new redis client -- and it all blows up.

Any thoughts on how to handle 3rd level dependencies from nuget at runtime?

Bryon Weber
  • 106
  • 1
  • 7
  • You may find some info here https://stackoverflow.com/questions/31859267/load-nuget-dependencies-at-runtime – Rex Aug 24 '17 at 18:08
  • This seems more about loading nuget packages in the current (whatever the .NET core/standard equivalent is) AppDomain rather than loading a dependency which has further dependencies. – Bryon Weber Aug 24 '17 at 18:40

3 Answers3

1

DLLs must be there in order to load them, AFAIK you shouldn't download nugget packages on the run, because it will be slow, and it can stop working at any time that the nugget origin is not available or, more probably, that you don't have internet connection.

So make your project depend on that nugget package, and it will be download before building.

If you're not interested on this approach, then I suppose you could try to execute NuGet.exe from your program and make it download the required DLL first, but this will make your program to hang up while it's downloading the package files.

Pablo Recalde
  • 3,334
  • 1
  • 22
  • 47
  • Well, the nuget repository is downloaded when Library B is built. So the DLLs exist on the system. But when Library A dynamically loads Library B (it even recognizes that Library B references Redis in `Assembly.GetReferencedAssemblies()`), it can't find the location of the DLL that Library B used to build with. I don't think nuget is the problem. I think that AssemblyLoadContext.Default.LoadFromAssemblyPath doesn't know how to load sub-dependencies. – Bryon Weber Aug 24 '17 at 18:30
  • 1
    An assembly has metadata corresponding to the assembly names of the references, but no paths. To load this references, you need a resolver, which will resolve the assembly. As the dependency dll is not in the same path as the LibraryB dll (it was not published), you need to get the path to the dependencies. As it's not published, I think that you need to load the LibraryB.runtimeconfig.dev.json file and LibraryB.deps.json and get the paths there. – José Pedro Aug 24 '17 at 18:52
1

What I ended up needing to do is add this in my project's csproj file: <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

Then adjust my module loader code to iterate through all of the DLLs and load them as well all before attempting to calling the constructor from my assembly via activator.

public void LoadExtensions()
{
    IConfigurationSection[] extensionConfigurations = _config.GetSections(EXTENSION_CONFIGURATION_KEY).ToArray();
    if (extensionConfigurations.Length == 0)
        return;

    HashSet<IExtension> extensions = new HashSet<IExtension>();
    foreach (IConfigurationSection extensionConfiguration in extensionConfigurations)
    {
        string name = extensionConfiguration.Key;
        string path = _config.Get($"{extensionConfiguration.Path}:path");

        _logger.Debug($"Loading extension: {name}");

        if (string.IsNullOrEmpty(path) || !File.Exists(path))
            throw new ConfigurationItemMissingException($"{extensionConfiguration.Path}:path");

        LoadAssembly(path, name);
    }

    foreach (var extensionType in _extensionTypes)
    {
        IExtension extension = Activator.CreateInstance(extensionType.Key.AsType(), extensionType.Value, _dependencyUtility) as IExtension;
        if (extension == null)
            throw new InvalidExtensionException(extensionType.Value, extensionType.Key.AssemblyQualifiedName);

        extensions.Add(extension);
    }

    Extensions = extensions;
}

private void LoadAssembly(string path, string name)
{
    FileInfo[] dlls = new DirectoryInfo(Path.GetDirectoryName(path)).GetFiles("*.dll");

    foreach (FileInfo dll in dlls)
    {
        Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(dll.FullName);

        _logger.Info($"Loading assembly: {asm.FullName}");

        TypeInfo type = asm.DefinedTypes.FirstOrDefault(x => x.ImplementedInterfaces.Contains(typeof(IExtension)) && !x.IsAbstract);

        if (type == null)
            continue;

        _extensionTypes.Add(type, name);
    }
}
Bryon Weber
  • 106
  • 1
  • 7
-1

You should not manually resolve assembly dependencies.

Just make sure that when you load your Library B dynamically all the dependent dll-s are reachable by the .net runtime. By default, it will check the Working Directory of your app process and GAC. If you want to customize runtime's probing behavior you can do it with <probing> setting in your config file or from within C# code.

I suggest you reading these docs, they should help you understand how probing works in greater details:

https://learn.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies

https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/specify-assembly-location

To troubleshoot the dependency resolution you can use fuslog tool:

https://learn.microsoft.com/en-us/dotnet/framework/tools/fuslogvw-exe-assembly-binding-log-viewer

Kostya
  • 849
  • 5
  • 14
  • Hmm. I figured this was how the probing worked -- but having Library B's dependencies in Library A's working directory didn't work. I'll take a look at fuslog, thanks! – Bryon Weber Aug 31 '17 at 13:03