原帖
应用需求1: 我们cad.net开发都会面临一个问题,加载了的dll无法实现覆盖操作, 就是cad一直打开的状态下,netload两次版本不一样的dll,它只会用第一次载入的….没法做到热插拔….
应用需求2: 我做了一个拖拉加载,不想发送netload到命令栏加载…
在这两个需求之下,已有的资料 明经netloadx 似乎是不二之选…
然而,提出上面的两个需求仅仅是我为了这篇文章想为什么需要这个技术而已…….编的 ( >,< )

真正令我开始研究是因为若海提出的: 明经netloadx 在 a.dll 引用了 b.dll 时候, 为什么不会成功调用…
我首先想到是依赖,于是乎,我试图尝试直接 Assembly.Load(File.ReadAllBytes(path)) 在加载目录的每个文件,并没有报错…
  这里打断一下,Assembly.Load(byte),转为byte是为了实现热插拔,所以Assembly.LoadForm()没有byte重载,也就无法拷贝到内存中去,故此不考虑…
然后出现了一个事情, 能使用单独的命令,却还是不能跨 dll 调用….也就是有运行出错…runtime error
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图1

然后我弄了好几天查找了一下那么多的资料……….还翻遍了微软各个函数………….
https://www.cnblogs.com/kongyiyun/archive/2011/08/01/2123459.html
https://www.cnblogs.com/zhuweisky/archive/2005/12/30/308218.html
https://www.cnblogs.com/mengnan/p/4811534.html
https://q.cnblogs.com/q/3530
https://vimsky.com/examples/detail/csharp-event-system.appdomain.assemblyresolve.html

最终,我还是问了群里的大佬,南胜回答了我,但是我用他的代码出现了几个问题:
他获取的路径是clr寻找路径之一,我需要改到加载路径上面的…这里各位可以自行去看看clr的寻找未知dll的方式…
以及他只支持一个引用的dll,而我需要知道引用的引用的引用的引用的引用的引用的引用的引用的引用…..的dll
所以需要对他的代码修改一番.

————————————————————-工程开始——————————————————————-

测试代码:
首先,一共有四个项目,
1: 直接netload的项目,也就是 [cad主插件项目].
2: testa [cad次插件]
3: testb [给a引用]
4: testc [给b引用]
testa 引用 testb , testb 引用 testc ….如果后面还有套娃也可以…..套娃的套娃的套娃的套娃的套娃….

[三个调用的CAD子插件项目]

testa项目代码:

  1. namespace testa
  2. {
  3. public class MyCommands
  4. {
  5. [CommandMethod("testa")]
  6. public static void testa()
  7. {
  8. Document doc = Acap.DocumentManager.MdiActiveDocument;
  9. Editor ed;
  10. if (doc != null)
  11. {
  12. ed = doc.Editor;
  13. ed.WriteMessage("\r\n自带函数testa.");
  14. }
  15. }
  16. [CommandMethod("gggg")]
  17. public void gggg()
  18. {
  19. Document doc = Acap.DocumentManager.MdiActiveDocument;
  20. Editor ed = doc.Editor;
  21. if (doc != null)
  22. {
  23. ed.WriteMessage("\r\n **********gggg");
  24. testb.MyCommands.TestBHello();
  25. }
  26. }
  27. }
  28. }

testb项目代码:

  1. namespace testb
  2. {
  3. public class MyCommands
  4. {
  5. public static void TestBHello()
  6. {
  7. Document doc = Acap.DocumentManager.MdiActiveDocument;
  8. Editor ed;
  9. if (doc != null)
  10. {
  11. ed = doc.Editor;
  12. ed.WriteMessage("************testb的Hello");
  13. testc.MyCommands.TestcHello();
  14. }
  15. }
  16. [CommandMethod("testb")]
  17. public static void testb()
  18. {
  19. Document doc = Acap.DocumentManager.MdiActiveDocument;
  20. Editor ed;
  21. if (doc != null)
  22. {
  23. ed = doc.Editor;
  24. ed.WriteMessage("\r\n自带函数testb.");
  25. }
  26. }
  27. }
  28. }

testc项目代码:

  1. namespace testc
  2. {
  3. public class MyCommands
  4. {
  5. public static void TestcHello()
  6. {
  7. Document doc = Acap.DocumentManager.MdiActiveDocument;
  8. Editor ed;
  9. if (doc != null)
  10. {
  11. ed = doc.Editor;
  12. ed.WriteMessage("************testc的Hello");
  13. }
  14. }
  15. [CommandMethod("testc")]
  16. public static void testc()
  17. {
  18. Document doc = Acap.DocumentManager.MdiActiveDocument;
  19. Editor ed;
  20. if (doc != null)
  21. {
  22. ed = doc.Editor;
  23. ed.WriteMessage("\r\n自带函数testc");
  24. }
  25. }
  26. }
  27. }

必须更改版本号最后是*,否则无法重复加载(所有)
net framework要直接编辑项目文件.csproj,启用由vs迭代版本号:

  1. <PropertyGroup>
  2. <Deterministic>False</Deterministic>
  3. </PropertyGroup>

然后修改AssemblyInfo.cs
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图2

net standard只需要增加.csproj的这里,没有自己加一个:

  1. <PropertyGroup>
  2. <AssemblyVersion>1.0.0.*</AssemblyVersion>
  3. <FileVersion>1.0.0.0</FileVersion>
  4. <Deterministic>False</Deterministic>
  5. </PropertyGroup>

[CAD主插件项目]

先说一下我的测试环境和概念,
我在[cad主插件]上面写了一个命令,这个命令调用了winform,winform让它接受拖拽dll文件,拿到dll的路径,然后链式加载..
这个时候需要直接启动cad,然后调用netload命令加载[cad插件]的dll.
如果采用vs调试cad启动的话,那么我们本来也这么想的…..
经过若海两天的Debug发现了: 不能在vs调试状态下运行cad!应该直接启动它!
猜想:这个时候令vs托管了cad的内存,令所有 Assembly.Load(byte) 都进入了托管内存上面,vs自动占用到 obj\Debug 文件夹下的dll.
(不信你也可以试一下) 我开了个新文章写这个问题

启动cad之后用命令调用出winform,再利用拖拽testa.dll的方式就可以链式加载到所有的dll了..
再修改testa.dll重新编译,再拖拽到winform加载,
再修改testb.dll重新编译,再拖拽到winform加载,
再修改testc.dll重新编译,再拖拽到winform加载
…..如此如此,这般这般…..

winform拖拽这个函数太复杂了,但是搜一下基本能搞定….我就不贴代码了.
接收拖拽之后就有个testa.dll的path,再调用传给加载函数就好了:

  1. var ad = new AssemblyDependent(path);
  2. var msg = ad.Load();
  3. bool allyes = true;
  4. foreach (var item in msg)
  5. {
  6. if (!item.LoadYes)
  7. {
  8. ed.WriteMessage(Environment.NewLine + "**" + item.Path +
  9. Environment.NewLine + "**此文件已加载过,重复名称,重复版本号,本次不加载!" +
  10. Environment.NewLine);
  11. allyes = false;
  12. }
  13. }
  14. if (allyes)
  15. {
  16. ed.WriteMessage(Environment.NewLine + "**链式加载成功!" + Environment.NewLine);
  17. }

链式加载:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Text;
  7. namespace JoinBoxCurrency
  8. {
  9. public class AssemblyDependent
  10. {
  11. string _dllFile;
  12. /// <summary>
  13. /// cad程序域依赖_内存区(不可以卸载)
  14. /// </summary>
  15. private Assembly[] _cadAs;
  16. /// <summary>
  17. /// cad程序域依赖_映射区(不可以卸载)
  18. /// </summary>
  19. private Assembly[] _cadAsRef;
  20. /// <summary>
  21. /// 当前域加载事件
  22. /// </summary>
  23. public event ResolveEventHandler CurrentDomainAssemblyResolveEvent;
  24. /// <summary>
  25. /// 加载dll的和相关的依赖
  26. /// </summary>
  27. /// <param name="dllFile"></param>
  28. public AssemblyDependent(string dllFile)
  29. {
  30. _dllFile = dllFile;
  31. //cad程序集的依赖
  32. _cadAs = AppDomain.CurrentDomain.GetAssemblies();
  33. //映射区
  34. _cadAsRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
  35. //运行时出错的话,就靠这个事件来解决
  36. if (CurrentDomainAssemblyResolveEvent != null)
  37. {
  38. AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolveEvent;
  39. }
  40. else
  41. {
  42. AppDomain.CurrentDomain.AssemblyResolve += RunTimeCurrentDomain.DefaultAssemblyResolve;
  43. }
  44. }
  45. /// <summary>
  46. /// 返回的类型,描述加载的错误
  47. /// </summary>
  48. public class LoadDllMessage
  49. {
  50. public string Path;
  51. public bool LoadYes;
  52. public LoadDllMessage(string path, bool loadYes)
  53. {
  54. Path = path;
  55. LoadYes = loadYes;
  56. }
  57. }
  58. /// <summary>
  59. /// 字节加载
  60. /// </summary>
  61. /// <param name="_dllFile"></param>
  62. /// <returns></returns>
  63. public LoadDllMessage[] Load()
  64. {
  65. var loadYesList = new List<LoadDllMessage>();
  66. if (!File.Exists(_dllFile))
  67. {
  68. return loadYesList.ToArray();
  69. }
  70. //查询加载链之后再逆向加载,确保前面不丢失
  71. var allRefs = GetAllRefPaths(_dllFile);
  72. allRefs.Reverse();
  73. foreach (var path in allRefs)
  74. {
  75. //路径转程序集名
  76. string assName = AssemblyName.GetAssemblyName(path).FullName;
  77. //路径转程序集名
  78. Assembly assembly = _cadAs.FirstOrDefault((Assembly a) => a.FullName == assName);
  79. if (assembly == null)
  80. {
  81. //为了实现debug时候出现断点,见链接
  82. // https://www.cnblogs.com/DasonKwok/p/10510218.html
  83. // https://www.cnblogs.com/DasonKwok/p/10523279.html
  84. //实现字节加载
  85. var buffer = File.ReadAllBytes(path);
  86. #if DEBUG
  87. var dir = Path.GetDirectoryName(path);
  88. var pdbName = Path.GetFileNameWithoutExtension(path) + ".pdb";
  89. var pdbFullName = Path.Combine(dir, pdbName);
  90. if (File.Exists(pdbFullName))
  91. {
  92. var pdbbuffer = File.ReadAllBytes(pdbFullName);
  93. Assembly.Load(buffer, pdbbuffer);//就是这句会占用vs生成,可能这个问题是net strandard
  94. }
  95. else
  96. {
  97. Assembly.Load(buffer);
  98. }
  99. #else
  100. Assembly.Load(buffer);
  101. #endif
  102. loadYesList.Add(new LoadDllMessage(path, true));//加载成功
  103. }
  104. else
  105. {
  106. loadYesList.Add(new LoadDllMessage(path, false));//版本号没变不加载
  107. }
  108. }
  109. return loadYesList.ToArray();
  110. }
  111. /// <summary>
  112. /// 获取加载链
  113. /// </summary>
  114. /// <param name="dll"></param>
  115. /// <param name="dlls"></param>
  116. /// <returns></returns>
  117. List<string> GetAllRefPaths(string dll, List<string> dlls = null)
  118. {
  119. dlls = dlls ?? new List<string>();
  120. //如果含有 || 不存在文件
  121. if (dlls.Contains(dll) || !File.Exists(dll))
  122. {
  123. return dlls;
  124. }
  125. dlls.Add(dll);
  126. //路径转程序集名
  127. string assName = AssemblyName.GetAssemblyName(dll).FullName;
  128. //在当前程序域的assemblyAs内存区和assemblyAsRef映射区找这个程序集名
  129. Assembly assemblyAs = _cadAs.FirstOrDefault((Assembly a) => a.FullName == assName);
  130. Assembly assemblyAsRef;
  131. //内存区有表示加载过
  132. //映射区有表示查找过但没有加载(一般来说不存在.只是debug会注释掉Assembly.Load的时候用来测试)
  133. if (assemblyAs != null)
  134. {
  135. assemblyAsRef = assemblyAs;
  136. }
  137. else
  138. {
  139. assemblyAsRef = _cadAsRef.FirstOrDefault((Assembly a) => a.FullName == assName);
  140. //内存区和映射区都没有的话就把dll加载到映射区,用来找依赖表
  141. assemblyAsRef = assemblyAsRef ?? Assembly.ReflectionOnlyLoad(File.ReadAllBytes(dll));
  142. }
  143. //遍历依赖,如果存在dll拖拉加载目录就加入dlls集合
  144. foreach (var assemblyName in assemblyAsRef.GetReferencedAssemblies())
  145. {
  146. //dll拖拉加载路径-搜索路径(可以增加到这个dll下面的所有文件夹?)
  147. string directoryName = Path.GetDirectoryName(dll);
  148. var path = directoryName + "\\" + assemblyName.Name;
  149. var paths = new string[]
  150. {
  151. path + ".dll",
  152. path + ".exe"
  153. };
  154. foreach (var patha in paths)
  155. {
  156. GetAllRefPaths(patha, dlls);
  157. }
  158. }
  159. return dlls;
  160. }
  161. }
  162. }

而其中最重要的是这个事件: 运行域事件
它会在运行的时候找已经载入内存上面的程序集.

  1. AppDomain.CurrentDomain.AssemblyResolve += RunTimeCurrentDomain.DefaultAssemblyResolve;
  1. using System;
  2. using System.Linq;
  3. using System.Reflection;
  4. namespace JoinBoxCurrency
  5. {
  6. public static class RunTimeCurrentDomain
  7. {
  8. #region 程序域运行事件
  9. // 动态编译要注意所有的引用外的dll的加载顺序
  10. // cad2008若没有这个事件,会使动态命令执行时候无法引用当前的程序集函数
  11. // 跨程序集反射
  12. // 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
  13. // netload会把所处的运行域给改到cad自己的,而动态编译不通过netload,所以要自己去改.
  14. // 这相当于是dll注入的意思,只是动态编译的这个"dll"不存在实体,只是一段内存.
  15. /// <summary>
  16. /// 程序域运行事件
  17. /// </summary>
  18. public static Assembly DefaultAssemblyResolve(object sender, ResolveEventArgs args)
  19. {
  20. var cad = AppDomain.CurrentDomain.GetAssemblies();
  21. #if false
  22. /*获取名称一致,但是版本号不同的,调用最开始的版本*/
  23. //获取执行程序集的参数
  24. var ag = args.Name.Split(',')[0];
  25. //获取 匹配符合条件的第一个或者默认的那个
  26. // var load = cad.FirstOrDefault(a => a.GetName().FullName.Split(',')[0] == ag);
  27. #endif
  28. /*获取名称和版本号都一致的,调用它*/
  29. Assembly load = null;
  30. load = cad.FirstOrDefault(a => a.GetName().FullName == args.Name);
  31. if (load == null)
  32. {
  33. /*获取名称一致,但是版本号不同的,调用最后的可用版本*/
  34. var ag = args.Name.Split(',')[0];
  35. //获取 最后一个符合条件的,
  36. //否则a.dll引用b.dll函数的时候,b.dll修改重生成之后,加载进去会调用第一个版本的b.dll
  37. foreach (var item in cad)
  38. {
  39. if (item.GetName().FullName.Split(',')[0] == ag)
  40. {
  41. //为什么加载的程序版本号最后要是*
  42. //因为vs会帮你迭代这个版本号,所以最后的可用就是循环到最后的.
  43. load = item;
  44. }
  45. }
  46. }
  47. return load;
  48. }
  49. #endregion
  50. }
  51. }

备注:
关于动态加载和动态编译是有相通的部分的,而这个部分就是这个事件…
cad2008若没有这个事件,会使动态编译的命令,在执行时候无法引用当前的程序集函数…等于runtime error
netload会把所处的运行域给改到cad自己的,而动态编译不通过 netload,所以要自己去改.

[调试]

另见 https://www.cnblogs.com/JJBox/p/14050111.html
本质上来说dll的动态加载目的就是为了不占用dll文件,不占用文件是为了重复编译,重复编译是为了不重启cad就可以载入插件来调试,那么vs要如何调试已经脱钩加载进来的东西呢?

[方法1]

高版本cad的话,利用[cad主插件项目]来启动cad(调试启动),然后动态加载进来,在cad上面敲命令,
恰逢你此时有开[cad次插件项目]就会捕捉到上面的.cs文件,如果没有开,vs会弹出这个对话框让你找代码文件,就选中cs文件就好了.
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图3
它就会击中断点了!!
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图4

[方法2]

直接启动cad(不调试)之后,把dll动态加载之后,选择附加进程调试Ctrl+Alt+P, 然后选择cad附加..即使当前的dll已经脱钩了,
但是仍然可以让vs链接上对应的文件来调试…..
我也没有懂为什么…..明明代码文件是属于无关的了….高版本却可以连接到,低版本就连接不到…….
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图5
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图6


cad08附加进程调试会这样..(我已经试过很多很多方法了….大家用高版本调试吧……
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图7
我试了多少种方法:…………………………
方法1:
打开 acad.exe.config,内容如下

  1. <configuration>
  2. <startup>
  3. <!--We always use the latest version of the framework installed on the computer. If you
  4. are having problems then explicitly specify .NET 2.0 by uncommenting the following line.
  5. <supportedRuntime version="v2.0.50727"/>
  6. -->
  7. </startup>
  8. </configuration>

改为:

  1. <?xml version="1.0"?>
  2. <configuration>
  3. <startup useLegacyV2RuntimeActivationPolicy="true">
  4. <supportedRuntime version="v2.0.50727"/>
  5. </startup>
  6. <runtime><generatePublisherEvidence enabled="false"/> </runtime>
  7. </configuration>

勾选启用本地代码调试(T),就可以 F5 直接调试。
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图8


方法2:
将编译的目标平台设置为X86而不是AnyCPU或者X64
https://www.cnblogs.com/jeffwongishandsome/p/How-To-Solve-App-Break-Mode-Problem.html

方法3:
cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错. - 图9


大家如果能成功也请告诉我…