使用RegistrationServices.RegisterAssembly时依赖项找不到的问题求助
开发插件解决方案,所有插件通过统一安装程序部署,每个插件实现System.Configuration.Install.Installer,由ManagedInstallerClass.InstallHelper执行安装。其中一个插件需要COM注册包含入口的MyInterface.dll,在Installer类中调用RegistrationServices.RegisterAssembly进行注册时,出现错误:无法加载程序集AddInAssistant, Version=19.23.516.10987, Culture=neutral, PublicKeyToken=null或其依赖项。该依赖DLL已存在于安装目录且版本匹配,但程序集绑定日志(FusLog)显示安装程序在自身目录和目标目录均未找到它,手动使用regasm.exe /codebase注册则无问题。
安装类调用代码
List<string> copiedFiles = CopyFiles(Session.InstallPath, installPath); var interfaceDllPath = copiedFiles.FirstOrDefault(cf => cf.EndsWith(@"\MyInterface.dll")); if (interfaceDllPath != null) { try { var comRegistrar = new ComRegistrar(interfaceDllPath); bool registerSuccess = comRegistrar.Register(); if (registerSuccess) { Context.LogMessage($"Successfully COM-registered DLL at {interfaceDllPath}"); Context.LogMessage("Enabling AddIn Auto-Load"); SetAutoLoadEntry(); } else { Context.LogMessage("Failed to com-register DLL (unknown reason)."); } } catch (Exception exception) { Context.LogMessage($"Error during com-registration: {exception}"); throw; } }
ComRegistrar实现代码
public class ComRegistrar { private readonly string _assemblyPath; public ComRegistrar(string assemblyPath) { if (string.IsNullOrWhiteSpace(assemblyPath)) throw new ArgumentException("Error in ComRegistrar: assemblyPath cannot be null or empty."); _assemblyPath = assemblyPath; } public bool Register() { if (String.IsNullOrEmpty(_assemblyPath)) { throw new ArgumentException("Error in ComRegistrar: assemblyPath cannot be null or empty."); } RegistrationServices registrationService = new RegistrationServices(); return registrationService.RegisterAssembly(Assembly.LoadFile(_assemblyPath), AssemblyRegistrationFlags.SetCodeBase); } public bool Unregister() { if (String.IsNullOrEmpty(_assemblyPath)) { throw new ArgumentException("Error in ComRegistrar: assemblyPath cannot be null or empty."); } RegistrationServices registrationService = new RegistrationServices(); registrationService.UnregisterAssembly(Assembly.LoadFile(_assemblyPath)); //Force GarbageCollect to close file handle on Assembly before uninstall. registrationService = null; GC.Collect(); GC.WaitForPendingFinalizers(); return true; } }
程序集绑定日志(FusLog)
*** Assembly Binder Log Entry (21.09.2023 @ 14:23:18) *** The operation failed. Bind result: hr = 0x80070002. The system cannot find the file specified. Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll Running under executable C:\1_Develop_\Repos\External.Interfaces\Interfaces.Installer\Interfaces.Installer\UI.TestShell\bin\Debug\TDM Interfaces Installer.exe --- A detailed error log follows. === Pre-bind state information === LOG: DisplayName = AddInAssistant, Version=19.23.516.10987, Culture=neutral, PublicKeyToken=null (Fully-specified) LOG: Appbase = file:///C:/1_Develop_/Repos/External.Interfaces/Interfaces.Installer/Interfaces.Installer/UI.TestShell/bin/Debug/ LOG: Initial PrivatePath = NULL LOG: Dynamic Base = NULL LOG: Cache Base = NULL LOG: AppName = TDM Interfaces Installer.exe Calling assembly : TDMInterface, Version=2023.0.0.1, Culture=neutral, PublicKeyToken=null. LOG: This bind starts in LoadFrom load context. WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load(). LOG: Using application configuration file: C:\1_Develop_\Repos\External.Interfaces\Interfaces.Installer\Interfaces.Installer\UI.TestShell\bin\Debug\TDM Interfaces Installer.exe.Config LOG: Using host configuration file: LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config. LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind). LOG: Attempting download of new URL file:///C:/1_Develop_/Repos/External.Interfaces/Interfaces.Installer/Interfaces.Installer/UI.TestShell/bin/Debug/AddInAssistant.DLL. LOG: Attempting download of new URL file:///C:/1_Develop_/Repos/External.Interfaces/Interfaces.Installer/Interfaces.Installer/UI.TestShell/bin/Debug/AddInAssistant/AddInAssistant.DLL. LOG: Attempting download of new URL file:///C:/1_Develop_/Repos/External.Interfaces/Interfaces.Installer/Interfaces.Installer/UI.TestShell/bin/Debug/AddInAssistant.EXE. LOG: Attempting download of new URL file:///C:/1_Develop_/Repos/External.Interfaces/Interfaces.Installer/Interfaces.Installer/UI.TestShell/bin/Debug/AddInAssistant/AddInAssistant.EXE. LOG: Attempting download of new URL file:///C:/Program Files (x86)/ParentSoftware/AddIns/MyPluginFolder/AddInAssistant.DLL. LOG: Attempting download of new URL file:///C:/Program Files (x86)/ParentSoftware/AddIns/MyPluginFolder/AddInAssistant/AddInAssistant.DLL. LOG: Attempting download of new URL file:///C:/Program Files (x86)/ParentSoftware/AddIns/MyPluginFolder/AddInAssistant.EXE. LOG: Attempting download of new URL file:///C:/Program Files (x86)/ParentSoftware/AddIns/MyPluginFolder/AddInAssistant/AddInAssistant.EXE. LOG: All probing URLs attempted and failed.
解决方案
问题根源
使用Assembly.LoadFile加载程序集时,CLR的LoadFrom上下文不会自动探测目标程序集所在目录的依赖项;而手动执行regasm.exe时,regasm会自动将当前工作目录切换到目标DLL所在路径,CLR会优先探测当前目录的依赖,因此不会出现加载失败。
方案1:临时切换工作目录到目标安装目录
注册前将当前工作目录切换到MyInterface.dll所在的安装目录,注册完成后恢复原目录,让CLR自动探测该目录下的依赖:
public bool Register() { if (String.IsNullOrEmpty(_assemblyPath)) { throw new ArgumentException("Error in ComRegistrar: assemblyPath cannot be null or empty."); } // 保存原始工作目录 var originalWorkingDir = Directory.GetCurrentDirectory(); var targetAssemblyDir = Path.GetDirectoryName(_assemblyPath); try { // 切换到目标程序集所在目录 Directory.SetCurrentDirectory(targetAssemblyDir); RegistrationServices registrationService = new RegistrationServices(); // 使用Assembly.LoadFrom代替LoadFile,更适合探测当前目录依赖 return registrationService.RegisterAssembly(Assembly.LoadFrom(_assemblyPath), AssemblyRegistrationFlags.SetCodeBase); } finally { // 恢复原始工作目录,避免影响其他操作 Directory.SetCurrentDirectory(originalWorkingDir); } }
优点:实现简单,完全利用CLR的默认探测逻辑,无需额外处理依赖加载;
注意:多线程场景下需注意工作目录切换的并发问题,确保同一时间只有一个操作切换目录。
方案2:手动处理依赖加载(AssemblyResolve事件)
通过订阅AppDomain.CurrentDomain.AssemblyResolve事件,在依赖程序集加载失败时手动从目标目录加载:
public bool Register() { if (String.IsNullOrEmpty(_assemblyPath)) { throw new ArgumentException("Error in ComRegistrar: assemblyPath cannot be null or empty."); } var targetAssemblyDir = Path.GetDirectoryName(_assemblyPath); // 注册AssemblyResolve事件 ResolveEventHandler resolveHandler = (sender, args) => { var assemblyName = new AssemblyName(args.Name); var assemblyPath = Path.Combine(targetAssemblyDir, $"{assemblyName.Name}.dll"); if (File.Exists(assemblyPath)) { return Assembly.LoadFrom(assemblyPath); } return null; }; AppDomain.CurrentDomain.AssemblyResolve += resolveHandler; try { RegistrationServices registrationService = new RegistrationServices(); return registrationService.RegisterAssembly(Assembly.LoadFile(_assemblyPath), AssemblyRegistrationFlags.SetCodeBase); } finally { // 取消事件订阅,避免内存泄漏 AppDomain.CurrentDomain.AssemblyResolve -= resolveHandler; } }
优点:无需切换工作目录,适合复杂场景下的依赖加载;
注意:需确保事件处理逻辑能正确匹配依赖程序集的名称和路径,避免加载错误版本。
方案3:调用regasm.exe进程代替RegistrationServices
直接启动regasm.exe进程执行注册操作,完全模拟手动注册的流程,自动处理所有依赖:
using System.Diagnostics; using System.Runtime.InteropServices; public bool Register() { if (String.IsNullOrEmpty(_assemblyPath)) { throw new ArgumentException("Error in ComRegistrar: assemblyPath cannot be null or empty."); } // 获取对应.NET版本的regasm路径 var regasmPath = Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "regasm.exe"); // 构造regasm参数:/codebase指定程序集路径,/silent静默执行 var arguments = $"\"{_assemblyPath}\" /codebase /silent"; var processStartInfo = new ProcessStartInfo(regasmPath, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, // 确保regasm以管理员权限运行(如果需要) Verb = "runas" }; using (var process = Process.Start(processStartInfo)) { process.WaitForExit(); if (process.ExitCode != 0) { var errorMsg = process.StandardError.ReadToEnd(); throw new InvalidOperationException($"Regasm注册失败: {errorMsg}"); } return true; } }
优点:兼容性最好,完全复现手动注册的成功逻辑,无需处理CLR加载上下文问题;
注意:需确保安装程序有足够权限执行regasm,且regasm路径对应正确的.NET版本(如32/64位)。
内容的提问来源于stack exchange,提问作者PouncingPiggy

