.NET Framework4.8动态加载插件时同目录Google.Protobuf.dll加载失败
基于.NET Framework 4.8开发的C#应用,使用ClearScript库支持JavaScript作为扩展语言,可将功能封装为DLL插件在运行时动态挂载,核心挂载逻辑代码如下:
JSE.Script.attach = (Func<string, bool>)Attach; ... private static bool Attach(string dllPath, string name = "") { var status = false; var htc = new HostTypeCollection(); try { var assem = Assembly.Load(AssemblyName.GetAssemblyName(dllPath)); htc.AddAssembly(assem); if (name.Length == 0) { name = assem.FullName.Split(',')[0]; } JSE.AddHostObject(name, htc); //FIXME checkout the hosttypes Console.Error.WriteLine($"Attached {dllPath} as {name}"); status = true; } catch (ReflectionTypeLoadException rtle) { foreach (var item in rtle.LoaderExceptions) { Console.Error.WriteLine(item.Message); T.Fail(item.Message); } } catch (FileNotFoundException fnfe) { Console.Error.WriteLine(fnfe.Message); T.Fail(fnfe.Message); } catch (Exception e) { Console.Error.WriteLine(e.Message); T.Fail(e.Message); } return status; }
插件调用方式
JavaScript脚本中通过如下形式调用插件能力:
attach(".\\Plugin_GoogleAds_Metrics.dll"); H = Plugin_GoogleAds_Metrics.GoogleAds_Metrics.Historical; H.EnableTrace("GAM"); ...
异常现象
执行插件的GetAccountInformation方法,运行至GoogleAdsServiceClient googleAdsService = client.GetService(Services.V11.GoogleAdsService);代码行时抛出错误,提示无法加载Google.Protobuf程序集,完整异常信息如下:
Exception has been thrown by the target of an invocation. at JScript global code (Script [23] [temp]:5:0) -> acc = H.GetAccountInformation(auths.Item1, 7273576109, true) at Microsoft.ClearScript.ScriptEngine.ThrowScriptError(IScriptEngineException scriptError) at Microsoft.ClearScript.Windows.WindowsScriptEngine.ThrowScriptError(Exception exception) at Microsoft.ClearScript.Windows.WindowsScriptEngine.<>c__DisplayClass57_0`1.<ScriptInvoke>b__0() at Microsoft.ClearScript.ScriptEngine.ScriptInvokeInternal[T](Func`1 func) at Microsoft.ClearScript.ScriptEngine.ScriptInvoke[T](Func`1 func) at Microsoft.ClearScript.Windows.WindowsScriptEngine.ScriptInvoke[T](Func`1 func) at Microsoft.ClearScript.Windows.WindowsScriptEngine.Execute(UniqueDocumentInfo documentInfo, String code, Boolean evaluate) at Microsoft.ClearScript.Windows.JScriptEngine.Execute(UniqueDocumentInfo documentInfo, String code, Boolean evaluate) at Microsoft.ClearScript.ScriptEngine.Evaluate(UniqueDocumentInfo documentInfo, String code, Boolean marshalResult) at Microsoft.ClearScript.ScriptEngine.Evaluate(DocumentInfo documentInfo, String code) at Microsoft.ClearScript.ScriptEngine.Evaluate(String documentName, Boolean discard, String code) at Microsoft.ClearScript.ScriptEngine.Evaluate(String documentName, String code) at Microsoft.ClearScript.ScriptEngine.Evaluate(String code) at RulesetRunner.Program.Run(JScriptEngine& jSE, String scriptText, Config cfg, Dictionary`2 settings) in C:\Users\bugma\Source\Repos\Present\BORR\RulesetRunner\RunManagementPartials.cs:line 72 Exception has been thrown by the target of an invocation. Exception has been thrown by the target of an invocation. Could not load file or assembly 'Google.Protobuf, Version=3.15.8.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604' or one of its dependencies. The system cannot find the file specified.
已完成排查项
- 项目已引用最新版本的Google.Ads.GoogleAds库
- 插件项目csproj文件中已设置AutoGenerateBindingRedirects为true
- 已在插件项目上下文中执行Add-BindingRedirect命令
- Plugin_GoogleAds_Metrics.dll与Google.Protobuf.dll确实位于同一目录下
补全程序集探测路径逻辑
你现在用Assembly.Load加载插件时,CLR默认不会主动扫描插件DLL所在目录下的依赖,哪怕依赖和插件放在同一个文件夹也不会识别,这是.NET动态加载插件最常见的坑。
在主程序入口初始化阶段,注册AssemblyResolve事件手动处理依赖加载,不要把这段逻辑写在Attach方法里:AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { var targetAsmName = new AssemblyName(args.Name).Name; // 替换为你实际存放插件的目录路径 var pluginRoot = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var targetPath = Path.Combine(pluginRoot, $"{targetAsmName}.dll"); return File.Exists(targetPath) ? Assembly.LoadFrom(targetPath) : null; };这里用
LoadFrom而不是Load,可以让CLR正确识别加载上下文,避免同程序集不同路径重复加载的问题。把绑定重定向配置移到主程序
你之前在插件项目里开的自动绑定重定向、执行的PowerShell重定向命令全部不生效——.NET Framework的绑定重定向规则只会读取主入口EXE的配置文件,类库项目的App.config、生成的绑定重定向配置会被CLR完全忽略。
打开主程序项目的App.config,在runtime节点下手动添加Google相关依赖的绑定重定向,版本号填你实际输出目录下对应DLL的真实版本,示例:<runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Google.Protobuf" publicKeyToken="a7d26565bac4d604" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-3.25.3.0" newVersion="3.25.3.0" /> </dependentAssembly> <!-- Google.Ads.GoogleAds、Google.Api.Common等其他相关依赖按相同格式补全 --> </assemblyBinding> </runtime>重新生成主程序后,确认输出目录下
[你的主程序名].exe.config文件里存在上述配置,不要只改插件项目的配置文件。校验依赖完整性和版本匹配
用ILSpy或Dependencies工具打开插件目录下的两个文件做校验:- 查看
Plugin_GoogleAds_Metrics.dll引用的Google.Protobuf版本号,和目录下实际存在的Google.Protobuf.dll版本号是否匹配 - 检查
Google.Protobuf.dll自身的依赖是否齐全,重点确认System.Memory、System.Runtime.CompilerServices.Unsafe这两个基础类库是否存在、版本是否匹配,很多时候Protobuf加载失败是因为它自身的依赖缺失,不是Protobuf本身找不到。
- 查看
预加载插件依赖避免懒加载异常
你当前的Attach方法只把插件本身的程序集加入了HostTypeCollection,依赖程序集没有提前加载到AppDomain时,ClearScript遍历宿主类型做映射、或者JIT编译插件方法时会触发隐性的加载失败。可以在加载完插件主DLL后,遍历插件目录下的所有相关DLL做预加载,避免调用时才触发懒加载报错。
内容的提问来源于stack exchange,提问作者bugmagnet

