Using a dependency from another project

I’m struggling to load a dll from a grasshopper plugin (from yak) inside of my Rhp.

Setup:

LINK_Dashboards.gha is shipped on public yak.

Our inhouse LINK_EP.Rhp needs dashboards.

Issue:

If I ship dashboards with our RHP, then the grasshopper plugin cannot load because “the dll is already loaded”.


An error occured during GHA assembly loading:
  Path: C:\Users\mm1013\AppData\Roaming\McNeel\Rhinoceros\packages\8.0\LINK_Dashboards\2.5.20173.22506\LINK_EP.Dashboards.gha
  Exception System.IO.FileLoadException: 
  Message: Assembly with same name is already loaded

If I try using assemblyResolve in rhp to find the .gha file, then Rhino won’t load the dll. I can find it with my own assemblyResolve but it seems that Rhino is scanning for ContextTypes before I get to intercept the call and thus just rejects my rhp to run at all.

System.IO.FileNotFoundException: Could not load file or assembly 'LINK_EP.Dashboards, Version=2.6.20368.16210, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
File name: 'LINK_EP.Dashboards, Version=2.6.20368.16210, Culture=neutral, PublicKeyToken=null'
   at System.Reflection.RuntimeAssembly.GetExportedTypes(QCallAssembly assembly, ObjectHandleOnStack retTypes)
   at System.Reflection.RuntimeAssembly.GetExportedTypes()
   at Rhino.PlugIns.PlugIn.CreateFromAssembly(Assembly pluginAssembly, Boolean displayDebugInfo, Boolean useRhinoDotNet)
Failed to load LINK_EP_LINK_RH plugin

Alternative: I tried shipping 2 rhp files: 1 that checks and tries to load dashboards and error handling + 1 that actually uses dashboards. That was a mess, but I got to get the following messages in my console:

Dashboard Checker: Found Dashboards DLL at C:\Users\mm1013\AppData\Roaming\McNeel\Rhinoceros\packages\8.0\Linkajou\2.6.20367-prerelease\Fallback\LINK_EP.Dashboards.dll

System.IO.FileNotFoundException: Could not load file or assembly 'LINK_EP.Dashboards, Version=2.6.20368.16210, Culture=neutral, PublicKeyToken=null' [... same as above]

I don’t see any way of making Rhino see the dependency on alternative locations even though I have this setup (and running)

(in this setup im actually shipping Fallback/dashboards.dll together with the rhp as a fallback in case user doesnt have the gh plugin.

public class LINK_RH_Plugin : Rhino.PlugIns.PlugIn
{
    /// <summary>
    /// Static constructor - runs BEFORE instance constructor and BEFORE GetExportedTypes()
    /// This ensures AssemblyResolve handler is registered early enough to catch Dashboards loading
    /// </summary>
    static LINK_RH_Plugin()
    {
        WL.Debug(LogCategories.UIRhino, "Static constructor: Registering AssemblyResolve handler");
        AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve_Dashboards_Static;
    }
}



 /// <summary>
 /// Resolves LINK_Dashboards assembly with fallback strategy:
 /// 1. Check if already loaded (avoid duplicate loading)
 /// 2. Yak packages: Read version from manifest.txt, try LINK_Dashboards.gha then .dll
 /// 3. Local fallback: Use Fallback\LINK_Dashboards.dll shipped with build (only if Yak doesn't exist)
 /// Note: Local DLL is in Fallback subfolder to prevent .NET auto-loading, ensuring Yak has priority
 /// </summary>
 private static Assembly OnAssemblyResolve_Dashboards_Static(object sender, ResolveEventArgs args)
 {
     // Log every assembly resolve attempt for debugging
     WL.Debug(LogCategories.FileIO, () => $"AssemblyResolve triggered for: {args.Name}");

     if (args.Name.Contains("resources", StringComparison.InvariantCultureIgnoreCase))
         return null;
     if (!args.Name.StartsWith("LINK_EP.Dashboards"))
         return null;

     WL.Debug(LogCategories.FileIO, () => $"Resolving LINK_EP.Dashboards assembly...");

     // PRIORITY 0: Check if assembly is already loaded
     var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies()
         .FirstOrDefault(a => a.FullName.StartsWith("LINK_EP.Dashboards"));

     if (loadedAssembly is not null)
     {
         WL.Debug(LogCategories.FileIO, () => $"LINK_Dashboards already loaded: {loadedAssembly.Location}");
         return loadedAssembly;
     }

     // PRIORITY 1: Try Yak packages folder
     string yakBasePath = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
         "McNeel",
         "Rhinoceros",
         "packages",
         $"{RhinoApp.ExeVersion}.0",
         "LINK_Dashboards");

     if (Directory.Exists(yakBasePath))
     {
         WL.Debug(LogCategories.FileIO, () => $"Searching for LINK_Dashboards in Yak packages: {yakBasePath}");

         // Try reading version from manifest.txt (Yak standard)
         string manifestPath = Path.Combine(yakBasePath, "manifest.txt");
         string version = null;

         if (File.Exists(manifestPath))
         {
             try
             {
                 version = File.ReadAllText(manifestPath).Trim();
                 WL.Debug(LogCategories.FileIO, () => $"Read version {version} from manifest.txt");
             }
             catch (Exception ex)
             {
                 WL.Error(LogCategories.FileIO, () => $"Failed to read manifest.txt: {ex.Message}");
             }
         }

         // If manifest.txt exists and has valid version, use it
         if (!string.IsNullOrWhiteSpace(version))
         {
             // Try .gha first (Grasshopper assembly extension)
             string ghaPath = Path.Combine(yakBasePath, version, "LINK_Dashboards.gha");
             if (File.Exists(ghaPath))
             {
                 try
                 {
                     WL.Debug(LogCategories.FileIO, () => $"Loaded LINK_Dashboards from Yak package (.gha v{version}): {ghaPath}");
                     return Assembly.LoadFrom(ghaPath);
                 }
                 catch (Exception ex)
                 {
                     WL.Error(LogCategories.FileIO, () => $"Failed to load {ghaPath}: {ex.Message}");
                 }
             }

             // Fallback to .dll in same Yak folder
             string dllPath = Path.Combine(yakBasePath, version, "LINK_Dashboards.dll");
             if (File.Exists(dllPath))
             {
                 try
                 {
                     WL.Debug(LogCategories.FileIO, () => $"Loaded LINK_Dashboards from Yak package (.dll v{version}): {dllPath}");
                     return Assembly.LoadFrom(dllPath);
                 }
                 catch (Exception ex)
                 {
                     WL.Error(LogCategories.FileIO, () => $"Failed to load {dllPath}: {ex.Message}");
                 }
             }

             WL.Warn(LogCategories.FileIO, () => $"LINK_Dashboards v{version} not found in Yak version folder");
         }
         else
         {
             // Fallback: manifest.txt doesn't exist or is empty - use highest version directory
             WL.Debug(LogCategories.FileIO, () => $"manifest.txt not found, scanning for highest version");

             var versionDirs = Directory.GetDirectories(yakBasePath)
                 .Select(Path.GetFileName)
                 .Select(v =>
                 {
                     bool ok = System.Version.TryParse(v, out var parsed);
                     return new { Version = parsed, IsValid = ok, FolderName = v };
                 })
                 .Where(x => x.IsValid)
                 .OrderByDescending(x => x.Version)
                 .ToList();

             foreach (var versionDir in versionDirs)
             {
                 // Try .gha first
                 string ghaPath = Path.Combine(yakBasePath, versionDir.FolderName, "LINK_Dashboards.gha");
                 if (File.Exists(ghaPath))
                 {
                     try
                     {
                         WL.Debug(LogCategories.FileIO, () => $"Loaded LINK_Dashboards from Yak package (.gha v{versionDir.FolderName}): {ghaPath}");
                         return Assembly.LoadFrom(ghaPath);
                     }
                     catch (Exception ex)
                     {
                         WL.Error(LogCategories.FileIO, () => $"Failed to load {ghaPath}: {ex.Message}");
                     }
                 }

                 // Fallback to .dll
                 string dllPath = Path.Combine(yakBasePath, versionDir.FolderName, "LINK_Dashboards.dll");
                 if (File.Exists(dllPath))
                 {
                     try
                     {
                         WL.Debug(LogCategories.FileIO, () => $"Loaded LINK_Dashboards from Yak package (.dll v{versionDir.FolderName}): {dllPath}");
                         return Assembly.LoadFrom(dllPath);
                     }
                     catch (Exception ex)
                     {
                         WL.Error(LogCategories.FileIO, () => $"Failed to load {dllPath}: {ex.Message}");
                     }
                 }
             }

             WL.Warn(LogCategories.FileIO, () => $"LINK_Dashboards not found in any Yak version folder");
         }

         // If Yak folder exists, don't try local fallback to avoid conflicts
         WL.Error(LogCategories.FileIO, "LINK_Dashboards found in Yak packages but failed to load");
         return null;
     }
     else
     {
         WL.Debug(LogCategories.FileIO, () => $"Yak packages folder not found: {yakBasePath}");

         // PRIORITY 2: Local fallback - LINK_EP.Dashboards.dll shipped with build (only if Yak doesn't exist)
         // Stored in Fallback subfolder to prevent .NET auto-loading
         string localDllPath = Path.Combine(
             Path.GetDirectoryName(typeof(LINK_RH_Plugin).Assembly.Location),
             "Fallback",
             "LINK_EP.Dashboards.dll");

         if (File.Exists(localDllPath))
         {
             try
             {
                 WL.Debug(LogCategories.FileIO, () => $"Loading LINK_Dashboards from local fallback: {localDllPath}");
                 return Assembly.LoadFrom(localDllPath);
             }
             catch (Exception ex)
             {
                 WL.Error(LogCategories.FileIO, () => $"Failed to load local fallback {localDllPath}: {ex.Message}");
             }
         }
         else
         {
             WL.Debug(LogCategories.FileIO, () => $"Local fallback LINK_EP.Dashboards.dll not found at: {localDllPath}");
         }

         // All attempts failed
         WL.Error(LogCategories.FileIO, "LINK_Dashboards could not be loaded from any location (Yak not found, local fallback failed)");
         return null;
     }
 }

My next attempt would be take out the dashboard functionality of the dashboards.gha into a dashboardsCore.dll and load that from both the gha and rhp. Hope that will solve it. Thank you Claude AI for providing this refactor.

This should solve it. I’ll update you in 9000 gpu hours.

4 hrs and a large refactor later.

Now it works for me: I made sure that all my shared stuff is in dlls and call those from rhp and gha. And make sure not to have any dlls that are the same as the rhp/gha (obviously, seen in hindsight)