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