Resolving dependencies fixed in .NET 7?

Hi there,

I’m wondering if someone can tell me whether the resolving of dependencies (i.e. specific versions between packages) has been resolved in the move to .NET 7.

Basically, I am again working on a Rhino/Grasshopper project and found a similar error that I’ve previously experienced “Member not found” or something similar. Basically, a package was being resolved by a package which alphabetically occurs before mine and hence a version of the dll was being loaded by this package which is based on an old version. When my package loads it does not load the dll in my package (i think because a dll with the same name has already been loaded).

Anyone know what the latest on this issue is?

Also curious if dlls can now be reloaded at runtime? I think previously you could not unload a dll and hence reload a new version.

1 Like

Gak! Yet another version of Net?! I’ve got 4 of them on my system now - these things are getting as bad as tribbles. We need one Net that subsumes them all.

You’ll be glad to hear .NET 8 is already out, and no doubt .NET 9 is going to be in preview soon as well.

Is this a DLL you create yourself? If so you could say rename it, or otherwise strong name it?

No I can’t, I’m pretty sure I have had the same issue with newtonsoft and system.json before where I was using a more recent version with new apis that would result in a runtime exception that the API could not be found because another package referenced an older version which got loaded first.

As I understand .net core allows loading dll at runtime in a given context so each package could have it’s own context to avoid loading conflicting versions. I’m just wondering if this has been done as part of the upgrade?

I would love to know this too, currently in Rhino 7 when I load the plugin we’ve developed, it all works fine. But I spent a long time the other day trying to figure out why it wouldn’t work for a user. Turned out they had another plugin installed that loaded before ours, and that other plugin had an older version of a dependency we also have in our plugin.

More recently, I was trying to onboard a new developer, they couldn’t even install the plugin because it failed to initialize. It couldn’t load our System.Text.Json dependency, even though they had a fresh install of Rhino 7 without any additional plugins. I managed to get it to work by copying the whole registry entry for the plugin (not just the Name and FileName as outlined in Rhino - Registering Plugins (Windows) (rhino3d.com)) from my machine (with some paths tweaked). I’m assuming, when it does the automatic loading of the other registry entries, it does it much later in the process after another version of System.Text.Json is loaded. But when all the settings are there it loads earlier in the process.

As it stands in Rhino 7, it appears to me that it’s just luck if your plugin works. Am I doing something wrong?

I don’t know if it is helpful. In my experience, it was always less problematic to run only a small middleware inside a hosting app such as Rhino. If such a small plugin/middleware does nothing more than communicating with a dedicated application (using IPC), invoking commands or maybe even hosting host-native GUI elements, you keep much more control over your own system and you don’t need to rely on specific framework versions and technologies. It depends, but as soon as your application becomes complex enough, it really improves maintainability and developing experience. However this depends a bit on how much you need to rely on host-application features, so it is not doable for anyone. BTW the “AssemblyLoadContext” feature of new .NET is still problematic, especially when GUI’s are involved. So, plugin architecture is still a pain in .NET. But you could try to lazy-load your dependencies within your own context.

@nathanletwory today I just tried to add Azure.Identity to my project for authentication and got the following exception trying to call APIs in the library.

Method not found: 'Void Azure.Core.TokenRequestContext..ctor(System.String[], System.String, System.String, System.String, Boolean)'.

I assume this is due to the fact I have included a more up-to-date version that the one which has potentially been loaded.

Not sure what the best solution is here…

It is hard to say.

When you are debugging with Visual Studio you could try setting a breakpoint before this function gets called, then check the Modules window (Debug > Windows) and check what module versions have been loaded.

Other than that I’ll have to defer to @curtisw for help with DLL loading mechanisms.

Looks like Azure.Core and Azure.Identitiy are loaded by Rhino, unsure what versions, I’ll try checking the versions now. Ideally we wouldn’t tie our dependencies to match that of Rhino.

So we have solved this by using our own AssemblyLoadContext. It’s not ideal, basically we need to types as defined in our contracts which need to be constructed from our context which has the correct versions of the assemblies loaded.

I’m wondering whether Rhino could have a global context which loads all Rhino dlls within some known ones that should not be loaded in other contexts. Then each plugin has it’s own context in which it loads its dependencies excluding those known ones. The in order to resolve the dependencies of a given plugin it first tries to load from its context then falls back to the global context (i.e. where it will find RhinoCommon). I guess if 2 plugins are using the same library for types it needs to share this may not work.

Maybe it’s too much effort, but I am surprised given our 2 plugins contain very little in terms of dependencies and we’ve hit this problem on both occasions that there isn’t many more people struggling with the same isse.

Hey @Mike26,

I’m glad you were able to resolve your issues, but yes it would be more ideal if Rhino, while running in .NET Core, would use separate AssemblyLoadContext for each plugin to support this scenario.

I have created RH-80178 to look into getting that in. I’m not sure if it will have to be opt-in or not, but we should be able to come up with something.

Cheers!
Curtis.

4 Likes

Hey @Mike26 any chance you could share a code snippet / more details of how you implemented this? Currently a bit stuck with the exact same issue.

Its not that simple but basically what we have is 3 projects:
MyGrasshopper.Contracts
MyGrasshopper.Core
MyGrasshopper.Plugin

In the contracts library we have only interfaces and basically these need to be defined for any logic you want to consume in the Plugins library.

The Core library references the Contracts library and implements the interfaces.

The Plugin project references both Contracts and Core, but the ReferenceOutputAssembly is set to false for Core so that you can’t actually use it in your code, but it is copied as part of build so that you can load it at runtime.

In the contracts project we have 1 “master” type which exposes everything, we call this ICore. And in the Core project we implement ICore to expose all the Core logic we require in the Plugin project.

The we just pretty much have verbatim followed this tutorial Create a .NET Core application with plugins - .NET | Microsoft Learn

The following is from the tutorial the only difference is our naming is a little different (as we aren’t loading multiple).

using System;
using System.Reflection;
using System.Runtime.Loader;

namespace AppWithPlugin
{
    class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public PluginLoadContext(string pluginPath)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }

        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;
        }
    }
}

So PlugInLoadContext was just removed and we new up the AssemblyDependencyResolver in a constructor (as we only have 1).

The constructor is

    public PluginLoadContext()
    {
        var pluginDll = Assembly.GetExecutingAssembly().Location;
        var coreDll = Path.Combine(Path.GetDirectoryName(pluginDll)!, "Core", 
"MyGrasshopper.Core.dll");
        _resolver = new AssemblyDependencyResolver(coreDll);
    }

Then we just have a helper method to create and instance of the “master” type Core

    public static ICore LoadCore()
    {
        var loadContext = new CoreLoadContext();
        var assembly = loadContext.LoadFromAssemblyName(new AssemblyName("MyGrasshopper.Core"));

        foreach (Type type in assembly.GetTypes())
        {
            if (typeof(ICore).IsAssignableFrom(type))
            {
                if (Activator.CreateInstance(type) is ICore result)
                {
                    return result;
                }
            }
        }

        throw new ReflectionTypeLoadException(
            new Type[] { typeof(ICore) },
            null,
            "Could not find ICore implementation"
        );
    }

Finally, we just threw it in the MyGrasshopperInfo type created by the grasshopper template as.

public class MyGrasshopperInfo : GH_AssemblyInfo
{
    internal static readonly ICore Core = CoreLoadContext.LoadCore();
}

Then anywhere we need to use logic from our Core library in code it is:

MyGrasshopperInfo.Core.MyLogic()
1 Like

I will just state getting it up an running is not so simple (and we have to do some further tricks due to using a source generator as well).

I guess the only other thing to note is the only external packages referenced by the Plugin project are Rhino/Grasshopper specific. I.e. no dependencies that have the potential to clash with other plugins.

Thanks for sharing, it is interesting to see different approaches.

I am thankful that for our CNC plugin we decided to completely switch .net version for Rhino 8.
So we only provide a net7.0 plugin for Rhino 8. The net 4.8 plugin from Rhino 7 does work in Rhino 8 but we stopped development bar bug fixes.

For us the 20% speed boost from net 7 was a real selling point for machine operators and as we are generally the only plugin used by our customers we felt it acceptable to mandate use of net7 which i believe is the default in Rhino 8.

I assume you are using this approach because you need your plugin to work with many other plugins some of which are 4.8 only and some of which are net7?

No, this solution is needed irrespective of which framework you are targeting. Rhino loads all dependencies in a global context (which resolve by name, not name and version). So if a dependency is loaded by Rhino or another plugin that is not the same version of yours some APIs may not exist or have changed and you’ll run into runtime errors in which a Type/Method cannot be found.

This was first triggered for us from another plugin commonly used in our company which uses the same dependency but an older version and the name of their plugin is alphabetically first. So their plugin get loaded first, Rhino loads the dependency, then when ours load it recognised the dependency was already load, then when our plugin tries to call methods/create types from the dependency they do not exist as an old version was loaded.

We also recently found this with Azure.Core.dll I think. I’m pretty sure Rhino ships with an older version we use so without this solution that is the only version that gets loaded and we are using APIs that were introduced in later versions.

1 Like

Oh but I think this solution might be .NET Core specific. We didn’t really dig into whether this works with .NET framework as we are not supporting it.