我们cad.net开发都会面临一个问题,加载了的dll无法实现覆盖操作,
也就是cad一直打开的状态下netload两次同名但版本不一样的dll,它只会用第一次载入的.
也没法做到热插拔...

应用需求2

制作一个拖拉dll到cad加载,但是不想通过发送netload到命令栏以 明文形式 加载...

在这两个需求之下,已有的资料 明经netloadx 似乎是不二之选...

提出上面的两个需求仅仅是我为了这篇文章想为什么需要这个技术而已.......编的 ( >,< )

真正令我开始研究是因为若海提出的: 明经netloadx 在 a.dll 引用了 b.dll 时候, 为什么不会成功调用...

我首先想到是依赖,于是乎,我试图尝试直接 Assembly.Load(File.ReadAllBytes(path)) 在加载目录的每个文件,并没有报错,
然后出现了一个情况,能使用单独的命令,却还是不能跨dll调用,也就是会有运行出错(runtime error).

注明: Assembly.Load(byte),转为byte是为了实现热插拔,Assembly.LoadForm()没有byte重载,也就无法拷贝到内存中去,故此不考虑.
如果手写过IOC容器的,应该对以上两个函数非常的熟悉才对.

我问了群里的大佬 南胜写了一篇文章回答了我 ,但是我用他的代码出现了几个问题:

  • 他获取的路径是clr寻找路径之一,我需要改到加载路径上面的...这里各位可以自行去看看clr的 寻找未知dll的方式 .
  • 以及他只支持一个引用的dll,而我需要知道引用的引用的引用的引用的引用的引用的引用的引用的引用...的dll.
  • 所以需要对他的代码修改一番.

    首先,一共有四个项目,

  • cad主插件项目:直接netload的项目.
  • cad次插件:testa,testb [给a引用],testc [给b引用],后面还有套娃也可以...
  • graph TD netload命令加载-->cad主插件-->加载-->cad次插件 cad次插件testA-->引用-->testB-->testC-->test....

    cad子插件项目

    testa项目代码

    namespace testa
        public class MyCommands
            [CommandMethod("testa")]
            public static void testa()
                Document doc = Acap.DocumentManager.MdiActiveDocument;
                Editor ed;
                if (doc != null)
                    ed = doc.Editor;
                    ed.WriteMessage("\r\n自带函数testa.");
            [CommandMethod("gggg")]
            public void gggg()
                Document doc = Acap.DocumentManager.MdiActiveDocument;
                Editor ed = doc.Editor;
                if (doc != null)
                    ed.WriteMessage("\r\n **********gggg");  
                    testb.MyCommands.TestBHello();
    

    testb项目代码

    namespace testb
        public class MyCommands
            public static void TestBHello()
                Document doc = Acap.DocumentManager.MdiActiveDocument;
                Editor ed;
                if (doc != null)
                    ed = doc.Editor;
                    ed.WriteMessage("************testb的Hello");
                    testc.MyCommands.TestcHello(); 
            [CommandMethod("testb")]
            public static void testb()
                Document doc = Acap.DocumentManager.MdiActiveDocument;
                Editor ed;
                if (doc != null)
                    ed = doc.Editor;
                    ed.WriteMessage("\r\n自带函数testb.");
    

    testc项目代码

    namespace testc
        public class MyCommands
            public static void TestcHello()
                Document doc = Acap.DocumentManager.MdiActiveDocument;
                Editor ed;
                if (doc != null)
                    ed = doc.Editor;
                    ed.WriteMessage("************testc的Hello");
            [CommandMethod("testc")]
            public static void testc()
                Document doc = Acap.DocumentManager.MdiActiveDocument;
                Editor ed;
                if (doc != null)
                    ed = doc.Editor;
                    ed.WriteMessage("\r\n自带函数testc");
    

    迭代版本号

    必须更改版本号最后是*,否则无法重复加载(所有)
    如果想加载时候动态修改dll的版本号,需要学习PE读写.(此文略)

    net framework要直接编辑项目文件.csproj,启用由vs迭代版本号:

    <PropertyGroup>
      <Deterministic>False</Deterministic>
    </PropertyGroup>
    

    然后修改AssemblyInfo.cs

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

    <PropertyGroup>
        <AssemblyVersion>1.0.0.*</AssemblyVersion> 
        <FileVersion>1.0.0.0</FileVersion>
        <Deterministic>False</Deterministic>
    </PropertyGroup>
    

    cad主插件项目

    先说一下我的测试环境和概念,

    cad主插件上面写了一个命令,这个命令调用了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的路径,再调用传给加载函数就好了.

    AssemblyDependent ad = new();
    List<LoadState> ls = new();
    ad.Load(item, ls);
    var msg = AssemblyDependent.PrintMessage(ls);
    if (msg != null)
        MessageBox.Show(msg, "加载完毕!");
        MessageBox.Show("无任何信息", "加载出现问题!");
    

    以下成员参与修改了一些bug,感谢.
    ひㄨㄨ那个ㄨㄨ

    #define HarmonyPatch
    #define HarmonyPatch_1
    namespace IFoxCAD.LoadEx;
     * 因为此处引用了 nuget的 Lib.Harmony
     * 所以单独分一个工程出来作为cad工程的引用
     * 免得污染了cad工程的纯洁
    #if HarmonyPatch_1
    [HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
    #endif
    public class AssemblyDependent : IDisposable
    #if HarmonyPatch
        //这个是不能删除的,否则就不执行了
        //HarmonyPatch hook method 返回 false 表示拦截原函数
        public static bool Prefix() { return false; }
    #endif
        #region 字段和事件
        /// <summary>
        /// 当前域加载事件,运行时出错的话,就靠这个事件来解决
        /// </summary>
        public event ResolveEventHandler CurrentDomainAssemblyResolveEvent
            add { AppDomain.CurrentDomain.AssemblyResolve += value; }
            remove { AppDomain.CurrentDomain.AssemblyResolve -= value; }
        /// <summary>
        /// 拦截cad的Loader异常:默认是<paramref name="false"/>
        /// </summary>
        public bool PatchExtensionLoader = false;
        #endregion
        #region 构造
        /// <summary>
        /// 链式加载dll依赖
        /// </summary>
        public AssemblyDependent()
            //初始化一次,反复load
            CurrentDomainAssemblyResolveEvent += AssemblyHelper.DefaultAssemblyResolve;
        #endregion
        #region 获取加载链
        /// <summary>
        /// 加载程序集
        /// </summary>
        /// <param name="dllFullName">dll的文件位置</param>
        /// <param name="loadStates">返回加载链</param>
        /// <param name="byteLoad">true字节加载,false文件加载</param>
        /// <returns> 参数 <paramref name="dllFullName"/> 加载成功标识
        /// <code> 链条后面的不再理会,因为相同的dll引用辨识无意义 </code>
        /// </returns>
        public bool Load(string? dllFullName, List<LoadState> loadStates, bool byteLoad = true)
            if (dllFullName == null)
                throw new ArgumentNullException(nameof(dllFullName));
            dllFullName = Path.GetFullPath(dllFullName);//相对路径要先转换
            if (!File.Exists(dllFullName))
                throw new ArgumentException("路径不存在");
            //程序集数组要动态获取(每次Load的时候),
            //否则会变成一个固定数组,造成加载了之后也不会出现成员
            var cadAssembly = AppDomain.CurrentDomain.GetAssemblies();
            var cadAssemblyRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
            List<string> allRefs = new();
            GetAllRefPaths(cadAssembly, cadAssemblyRef, dllFullName, allRefs);
            bool dllFullNameLoadOk = false;
            //查询加载链逆向加载,确保前面不丢失
            //这里有问题,从尾巴开始的,就一定是没有任何引用吗?
            for (int i = allRefs.Count - 1; i >= 0; i--)
                var allRef = allRefs[i];
                //路径转程序集名
                var an = AssemblyName.GetAssemblyName(allRef).FullName;
                var assembly = cadAssembly.FirstOrDefault(a => a.FullName == an);
                if (assembly != null)
                    loadStates.Add(new LoadState(allRef, false));//版本号没变不加载
                    continue;
                //有一次true,就是true 
                if (allRef == dllFullName)
                    dllFullNameLoadOk = true;
                    var ass = GetPdbAssembly(allRef);
                    if (ass == null)
                        if (byteLoad)
                            ass = Assembly.Load(File.ReadAllBytes(allRef));
                            ass = Assembly.LoadFile(allRef);
                    loadStates.Add(new LoadState(allRef, true, ass));/*加载成功*/
                catch { loadStates.Add(new LoadState(allRef, false));/*错误造成*/ }
            return dllFullNameLoadOk;
        /// <summary>
        /// 在debug模式的时候才获取PBD调试信息
        /// </summary>
        /// <param name="path"></param>
        /// <param name="byteLoad"></param>
        /// <returns></returns>
        Assembly? GetPdbAssembly(string? path)
    #if DEBUG
            //为了实现Debug时候出现断点,见链接,加依赖
            // https://www.cnblogs.com/DasonKwok/p/10510218.html
            // https://www.cnblogs.com/DasonKwok/p/10523279.html
            var dir = Path.GetDirectoryName(path);
            var pdbName = Path.GetFileNameWithoutExtension(path) + ".pdb";
            var pdbFullName = Path.Combine(dir, pdbName);
            if (File.Exists(pdbFullName))
                return Assembly.Load(File.ReadAllBytes(path), File.ReadAllBytes(pdbFullName));
    #endif
            return null;
        /// <summary>
        /// 递归获取加载链
        /// </summary>
        /// <param name="cadAssembly">程序集_内存区</param>
        /// <param name="cadAssemblyRef">程序集_映射区</param>
        /// <param name="dllFullName">dll文件位置</param>
        /// <param name="dllFullNamesOut">返回的集合</param>
        /// <returns></returns>
        void GetAllRefPaths(Assembly[] cadAssembly,
                            Assembly[] cadAssemblyRef,
                            string? dllFullName,
                            List<string> dllFullNamesOut)
            if (dllFullName == null)
                throw new ArgumentNullException(nameof(dllFullName));
            if (dllFullNamesOut.Contains(dllFullName) || !File.Exists(dllFullName))
                return;
            dllFullNamesOut.Add(dllFullName);
            var assemblyAsRef = GetAssembly(cadAssembly, cadAssemblyRef, dllFullName);
            if (assemblyAsRef == null)
                return;
            var sb = new StringBuilder();
            //dll拖拉加载路径-搜索路径(可以增加到这个dll下面的所有文件夹?)
            sb.Append(Path.GetDirectoryName(dllFullName));
            sb.Append("\\");
            //遍历依赖,如果存在dll拖拉加载目录就加入dlls集合
            var asse = assemblyAsRef.GetReferencedAssemblies();
            for (int i = 0; i < asse.Length; i++)
                var path = sb.ToString() + asse[i].Name;
                var paths = new string[]
                    path + ".dll",
                    path + ".exe"
                for (int j = 0; j < paths.Length; j++)
                    GetAllRefPaths(cadAssembly, cadAssemblyRef, paths[j], dllFullNamesOut);
        /// <summary>
        /// 在内存区和映射区找已经加载的程序集
        /// </summary>
        /// <param name="cadAssembly">程序集_内存区</param>
        /// <param name="cadAssemblyRef">程序集_映射区</param>
        /// <param name="dllFullName">dll文件位置</param>
        /// <returns></returns>
        Assembly? GetAssembly(Assembly[] cadAssembly,
                              Assembly[] cadAssemblyRef,
                              string? dllFullName)
            //路径转程序集名
            var assName = AssemblyName.GetAssemblyName(dllFullName).FullName;
            //在当前程序域的 assemblyAs内存区 和 assemblyAsRef映射区 找这个程序集名
            var assemblyAs = cadAssembly.FirstOrDefault(ass => ass.FullName == assName);
            //内存区有表示加载过
            //映射区有表示查找过,但没有加载(一般来说不存在.只是debug会注释掉 Assembly.Load 的时候用来测试)
            if (assemblyAs != null)
                return assemblyAs;
            //映射区
            var assemblyAsRef = cadAssemblyRef.FirstOrDefault(ass => ass.FullName == assName);
            //内存区和映射区都没有的话就把dll加载到映射区,用来找依赖表
            if (assemblyAsRef != null)
                return assemblyAsRef;
            var byteRef = File.ReadAllBytes(dllFullName);
            if (PatchExtensionLoader)
    #if HarmonyPatch_1
                /* QQ1548253108:这里会报错,他提供了解决方案.
                 * 方案一:
                 * 在类上面加 [HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
                const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
                Harmony hm = new(ext);
                hm.PatchAll();
                assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
                hm.UnpatchAll(ext);
    #endif
    #if HarmonyPatch_2
                //方案二:跟cad耦合了
                const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
                var docAss = typeof(Autodesk.AutoCAD.ApplicationServices.Document).Assembly;
                var a = docAss.GetType(ext);
                var b = a.GetMethod("OnAssemblyLoad");
                Harmony hm = new(ext);
                hm.Patch(b, new HarmonyMethod(GetType(), "Dummy"));
                assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
                hm.UnpatchAll(ext);
    #endif
                 * 0x01 此句没有依赖会直接报错 
                 *      assemblyAsRef = Assembly.ReflectionOnlyLoad(dllFullName);
                 * 0x02 重复加载无修改的同一个dll,会出现如下异常:
                 *      System.IO.FileLoadException:
                 *      “API 限制: 程序集“”已从其他位置加载。无法从同一个 Appdomain 中的另一新位置加载该程序集。”
                 *      catch 兜不住的,仍然会在cad上面打印,原因是程序集数组要动态获取(已改)
                    assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
                catch (System.IO.FileLoadException)
            return assemblyAsRef;
        /// <summary>
        /// 加载信息
        /// </summary>
        public static string? PrintMessage(List<LoadState> loadStates)
            if (loadStates == null)
                return null;
            var sb = new StringBuilder();
            var ok = loadStates.FindAll(a => a.State);
            var no = loadStates.FindAll(a => !a.State);
            if (ok.Count != 0)
                sb.Append("** 这些文件加载成功!");
                foreach (var item in ok)
                    sb.Append(Environment.NewLine);
                    sb.Append("++ ");
                    sb.Append(item.DllFullName);
                sb.Append(Environment.NewLine);
                sb.Append(Environment.NewLine);
            if (no.Count != 0)
                sb.Append("** 这些文件已被加载过,同时重复名称和版本号,跳过!");
                foreach (var item in no)
                    sb.Append(Environment.NewLine);
                    sb.Append("-- ");
                    sb.Append(item.DllFullName);
            return sb.ToString();
        #endregion
        #region 删除文件 
        /// <summary>
        /// Debug的时候删除obj目录,防止占用
        /// </summary>
        /// <param name="dllFullName">dll文件位置</param>
        public void DebugDelObjFiles(string dllFullName)
            var filename = Path.GetFileNameWithoutExtension(dllFullName);
            var path = Path.GetDirectoryName(dllFullName);
            var pdb = path + "\\" + filename + ".pdb";
            if (File.Exists(pdb))
                File.Delete(pdb);
            var list = path.Split('\\');
            if (list[list.Length - 1] == "Debug" && list[list.Length - 2] == "bin")
                var projobj = path.Substring(0, path.LastIndexOf("bin")) + "obj";
                FileEx.DeleteFolder(projobj);
        #endregion
        #region 移动文件
        /// <summary>
        /// Debug的时候移动obj目录,防止占用
        /// </summary>
        public void DebugMoveObjFiles(string? dllFullName, Action action)
            // 临时文件夹_pdb的,无论是否创建这里都应该进行删除
            const string Temp = "Temp";
            string? temp_Pdb_dest = null;
            string? temp_Pdb_source = null;
            string? temp_Obj_dest = null; ;
            string? temp_Obj_source = null;
                var filename = Path.GetFileNameWithoutExtension(dllFullName);
                var path = Path.GetDirectoryName(dllFullName);
                //新建文件夹_临时目录
                temp_Pdb_dest = path + $"\\{Temp}\\";
                //移动文件进去
                temp_Pdb_source = path + "\\" + filename + ".pdb";
                FileEx.MoveFolder(temp_Pdb_source, temp_Pdb_dest);
                //检查是否存在obj文件夹,有就递归移动
                var list = path.Split('\\');
                if (list[list.Length - 1] == "Debug" && list[list.Length - 2] == "bin")
                    var proj = path.Substring(0, path.LastIndexOf("bin"));
                    temp_Obj_source = proj + "obj";
                    temp_Obj_dest = proj + $"{Temp}";
                    FileEx.MoveFolder(temp_Obj_source, temp_Obj_dest);
                action.Invoke();
            finally
                // 还原移动
                FileEx.MoveFolder(temp_Pdb_dest, temp_Pdb_source);
                FileEx.DeleteFolder(temp_Pdb_dest);
                FileEx.MoveFolder(temp_Obj_dest, temp_Obj_source);
                FileEx.DeleteFolder(temp_Obj_dest);
        #endregion
        #region Dispose
        public bool Disposed = false;
        /// <summary>
        /// 显式调用Dispose方法,继承IDisposable
        /// </summary>
        public void Dispose()
            //由手动释放
            Dispose(true);
            //通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
            GC.SuppressFinalize(this);
        /// <summary>
        /// 析构函数,以备忘记了显式调用Dispose方法
        /// </summary>
        ~AssemblyDependent()
            Dispose(false); //由系统释放
        /// <summary>
        /// 释放
        /// </summary>
        /// <param name="ing"></param>
        protected virtual void Dispose(bool ing)
            if (Disposed) return; //不重复释放
            Disposed = true;//让类型知道自己已经被释放
            CurrentDomainAssemblyResolveEvent -= AssemblyHelper.DefaultAssemblyResolve;
        #endregion
    /// <summary>
    /// 加载程序集和加载状态
    /// </summary>
    public struct LoadState
        public Assembly? Assembly;
        public string DllFullName;
        public bool State;
        public LoadState(string dllFullName, bool state, Assembly? assembly = null)
            DllFullName = dllFullName;
            State = state;
            Assembly = assembly;
    public class FileEx
        /// <summary>
        /// 判断含有文件名和后缀
        /// </summary>
        /// <param name="pathOrFile">路径或者完整文件路径</param>
        static bool ContainFileName(string? pathOrFile)
            // 判断输入的是单文件,它可能不存在
            var a = Path.GetDirectoryName(pathOrFile);
            var b = Path.GetFileName(pathOrFile);
            var c = Path.GetExtension(pathOrFile);
            // 是文件
            return a.Length > 0 && b.Length > 0 && c.Length > 0;
        /// <summary>
        /// 移动文件夹中的所有文件夹与文件到另一个文件夹
        /// </summary>
        /// <param name="sourcePathOrFile">源文件夹</param>
        /// <param name="destPath">目标文件夹</param>
        public static void MoveFolder(string? sourcePathOrFile, string? destPath)
            if (sourcePathOrFile is null)
                throw new ArgumentException(nameof(sourcePathOrFile));
            if (destPath is null)
                throw new ArgumentException(nameof(destPath));
            if (ContainFileName(destPath))
                destPath = Path.GetDirectoryName(destPath);
            //目标目录不存在则创建
            if (!Directory.Exists(destPath))
                Directory.CreateDirectory(destPath);
            if (ContainFileName(sourcePathOrFile))
                // 如果是单个文件,就移动到目录就好了
                if (File.Exists(sourcePathOrFile))
                    destPath += "\\" + Path.GetFileName(sourcePathOrFile);
                    File.Move(sourcePathOrFile, destPath);
                    return;
                return;
            // 如果是文件就改为路径
            if (!Directory.Exists(sourcePathOrFile))
                sourcePathOrFile = Path.GetDirectoryName(sourcePathOrFile);
                if (!Directory.Exists(sourcePathOrFile))
                    throw new DirectoryNotFoundException("源目录不存在!");
            MoveFolder2(sourcePathOrFile, destPath);
        /// <summary>
        /// 移动文件夹中的所有文件夹与文件到另一个文件夹
        /// </summary>
        /// <param name="sourcePath">源文件夹</param>
        /// <param name="destPath">目标文件夹</param>
        static void MoveFolder2(string sourcePath, string destPath)
            //目标目录不存在则创建
            if (!Directory.Exists(destPath))
                Directory.CreateDirectory(destPath);
            //获得源文件下所有文件
            var files = new List<string>(Directory.GetFiles(sourcePath));
            files.ForEach(c => {
                string destFile = Path.Combine(destPath, Path.GetFileName(c));
                //覆盖模式
                if (File.Exists(destFile))
                    File.Delete(destFile);
                File.Move(c, destFile);
            //获得源文件下所有目录文件
            List<string> folders = new(Directory.GetDirectories(sourcePath));
            folders.ForEach(c => {
                string destDir = Path.Combine(destPath, Path.GetFileName(c));
                //Directory.Move必须要在同一个根目录下移动才有效,不能在不同卷中移动。
                //Directory.Move(c, destDir);
                //采用递归的方法实现
                MoveFolder2(c, destDir);
        /// <summary>
        /// 递归删除文件夹目录及文件
        /// </summary>
        /// <param name="dir"></param>
        /// <returns></returns>
        public static void DeleteFolder(string? dir)
            if (dir is null)
                throw new ArgumentException(nameof(dir));
            if (!Directory.Exists(dir)) //如果存在这个文件夹删除之
                return;
            foreach (string d in Directory.GetFileSystemEntries(dir))
                if (File.Exists(d))
                    File.Delete(d); //直接删除其中的文件
                    DeleteFolder(d); //递归删除子文件夹
            Directory.Delete(dir, true); //删除已空文件夹
    

    运行域事件

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

    AppDomain.CurrentDomain.AssemblyResolve += AssemblyHelper.DefaultAssemblyResolve;
    AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += AssemblyHelper.ReflectionOnlyAssemblyResolve;
    
    namespace IFoxCAD.LoadEx;
    using System.Diagnostics;
    public class AssemblyHelper
        public static Assembly? ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs e)
            var cadAss = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
            return Resolve(cadAss, sender, e);
        /// <summary>
        /// <code>
        /// 程序域运行事件
        /// 这相当于是dll注入的意思,只是动态加载的这个"dll"不存在实体,只是一段内存.
        /// 它总是被 <seealso cref="AppDomain.CurrentDomain.AssemblyResolve"/>事件使用
        /// 0x01 动态加载要注意所有的引用外的dll的加载顺序
        /// 0x02 指定版本: Acad2008若没有这个事件,会使动态命令执行时候无法引用当前的程序集函数
        /// 0x03 目录构成: 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
        /// 0x04 命令构成: cad自带的netload会把所处的运行域给改到cad自己的,而动态加载不通过netload,所以要自己去改.
        /// </code>
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <returns>程序集如果为空就不会调用</returns>
        public static Assembly? DefaultAssemblyResolve(object sender, ResolveEventArgs e)
            var cadAss = AppDomain.CurrentDomain.GetAssemblies();
            return Resolve(cadAss, sender, e);
        public static Assembly? Resolve(Assembly[] cadAss, object sender, ResolveEventArgs e)
            // 名称和版本号都一致的,调用它
            var result = cadAss.FirstOrDefault(ass => ass.GetName().FullName == e.Name);
            if (result != null)
                return result;
            // 获取名称一致,但是版本号不同的,调用最后的可用版本
            var ag = GetAssemblyName(e.Name);
            // 获取最后一个符合条件的,
            // 否则a.dll引用b.dll函数的时候,b.dll修改重生成之后,加载进去会调用第一个版本的b.dll,
            // vs会迭代程序版本号的*,所以最后的可用就是循环到最后的.
            for (int i = 0; i < cadAss.Length; i++)
                if (GetAssemblyName(cadAss[i].GetName().FullName) == ag)
                    result = cadAss[i];
            if (result == null)
                // 惊惊: cad21+vs22 容易触发这个资源的问题
                // https://stackoverflow.com/questions/4368201/
                string[] fields = e.Name.Split(',');
                string name = fields[0];
                string culture = fields[2];
                if (name.EndsWith(".resources") && !culture.EndsWith("neutral"))
                    return null;
            if (result == null)
                var sb = new StringBuilder();
                sb.AppendLine($"{nameof(LoadEx)}------------------------------------------------------------");
                sb.AppendLine(nameof(DefaultAssemblyResolve) + "出错,程序集无法找到它");
                sb.AppendLine("++参数名:: " + GetAssemblyName(e.Name));
                sb.AppendLine("++参数完整信息:: " + e.Name);
                for (int i = 0; i < cadAss.Length; i++)
                    sb.AppendLine("-------匹配对象:: " + GetAssemblyName(cadAss[i].GetName().FullName));
                sb.AppendLine($"程序集找不到,遇到无法处理的错误,杀死当前进程!");
                sb.AppendLine($"{nameof(LoadEx)}------------------------------------------------------------");
                Debug.WriteLine(sb.ToString());
                Process.GetCurrentProcess().Kill();
                //Debugger.Break();
            return result;
        static string GetAssemblyName(string argString)
            return argString.Substring(0, argString.IndexOf(','));
    

    另见 cad.net dll动态加载之后如何调试

    卸载DLL(20210430补充,同时更改了上面的链式加载)

    卸载需要修改工程结构,并且最后发生了一些问题没能解决.

  • cad主插件工程,引用-->通讯类工程
  • 通讯类工程(继承MarshalByRefObject接口的)
  • 其他需要加载的子插件工程:cad子插件项目作为你测试加载的dll,里面有一个cad命令gggg,它将会用来验证我们是否成功卸载的关键.
  • 和以往的工程都不一样的是,我们需要复制一份acad.exe目录的所有文件到一个非C盘目录,如下:

    修改 主工程,属性页:

    生成,输出: G:\AutoCAD 2008\ <--由于我的工程是.net standard,所以这里将会生成各类net文件夹

    调试,可执行文件: G:\AutoCAD 2008\net35\acad.exe <--在net文件夹放acad.exe目录所有文件

    为什么?因为通讯类.dll必须要在acad.exe旁边,否则无法通讯,会报错,至于有没有其他方法,我不知道...

    通讯结构图

    graph TD cad主插件工程-->主域-->新域-->创建域-->创建通讯类-->通讯类 通讯类-->执行链式加载子插件工程-->新域程序集 通讯类-->新域程序集 新域-->卸载域-->破坏通讯类-->通讯类 主域-->跨域执行方法-->检查通讯类生命-->通讯类

    程序创建的时候就会有一个主域,然后我们需要在主域上面创建: 新域

    然后新域上创建通讯类,利用通讯类在新域进行链式加载,这样程序集都会在新域上面,

    这样主域就能够调用新域程序集的方法了.

    GC无法穿透释放的BUG

    在新域上面创建通讯类--卸载成功,但是关闭cad程序的时候弹出了报错:

    System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
    

    猜测是因为cad的dll调用了非托管对象,所以有内存等待GC释放,从而导致此错误.
    而这个GC是cad内部构建的,没有提供给二次开发操作(即使是Arx),自然无法干掉它了.

    为了验证,实现一个没引用cad.dll的dll,是可以卸载,且不会报GC错误.

    cad主程序dll工程

    using RemoteAccess;
    using System.Diagnostics;
    using Autodesk.AutoCAD.Runtime;
    using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
    using Exception = System.Exception;
    // 载入dll内的cad的命令
    // 1 利用动态编译在主域下增加命令.
    // 2 挂未知命令反应器,找不到就去域内找(比较简单,这个实验成功)
    namespace JoinBox
        public class LoadAcadDll : IAutoGo
            public Sequence SequenceId()
                return Sequence.Last;
            public void Terminate()
                // 不可以忽略,因为直接关闭cad的时候是通过这里进行析构,而且优先于析构函数.
                // 而析构对象需要本类 _jAppDomain 提供域,否则拿不到.
                JJUnLoadAppDomain();
            /// <summary>
            /// 用于和其他域通讯的中间类
            /// </summary>
            public static JAppDomain _jAppDomain = null;
            /// <summary>
            /// 命令卸载程序域
            /// </summary>
            [CommandMethod("JJUnLoadAppDomain")]
            public void JJUnLoadAppDomain()
                if (_jAppDomain == null)
                    return;
                _jAppDomain.Dispose();
                _jAppDomain = null;
            [CommandMethod("测试无引用cad的dll的命令")]
            public void 测试无引用cad的dll的命令()
                if (_jAppDomain == null)
                    return;
                // 这样调用是成功的,这个dll没有用到cad的东西,所以GC释放很成功
                var aa = _jAppDomain.JRemoteLoader.Invoke("客户端.HelloWorld", "GetTime", new object[] { "我是皮卡丘" });
                System.Windows.Forms.MessageBox.Show(aa.ToString());
            /// <summary>
            /// 未知命令就跑其他程序集找然后调用,测试我们卸载之后gggg命令是否仍然有用
            /// </summary>
            public void CmdUnknown()
                var dm = Acap.DocumentManager;
                var md = dm.MdiActiveDocument;
                // 反应器->未知命令
                md.UnknownCommand += (sender, e) =>
                    if (_jAppDomain == null)
                        return;
                    // 这里可能产生:不可序列化的错误
                    // 因为cad域需要和其他域沟通,那么cad域的变量都无法穿透过去
                    // 所以需要以参数封送到远程通讯类上,再发送到其他域
                    var globalCommandName = e.GlobalCommandName;
                    var jrl = _jAppDomain.JRemoteLoader;
                    jrl?.TraversideLoadAssemblys((jrl2, assembly, ars) =>
                            var cmd = ars[0].ToString().ToUpper();
                            var caddll = new LoadAcadCmds(assembly);
                            if (caddll.AcadDllInfos.ContainsKey(cmd))
                                var info = caddll.AcadDllInfos[cmd];
                                var sp = info.Namespace + "." + info.ClassName;
                                var returnValue = jrl2.Invoke(assembly, sp, cmd);
                                return true;//returnValue可能无返回值,但是这里仍然结束循环
                        catch (Exception e)
                            Debug.WriteLine(e.Message);
                        return null;
                    }, new object[] { globalCommandName });
            public void Initialize()
                // 此dll拥有引用的dll,引用的dll,引用的dll....
                string dll = @"H:\解决方案\动态加载项目\若海加载项目增加卸载版\ClassLibrary1\bin\Debug\net35\ClassLibrary1.dll";//cad的类,会发生GC穿透
                dll = @"H:\解决方案\动态加载项目\若海加载项目增加卸载版\客户端\bin\Debug\客户端.dll";//无调用cad的类,就可以卸载
                    _jAppDomain = new JAppDomain("MyJoinBoxAppDomain", dll);
                    var jrl = _jAppDomain.JRemoteLoader;
                    // 载入cad的命令之后是否可以在这个域内呢
                    jrl.LoadAssembly(dll);
                    //加载不成功就结束掉好了
                    if (!jrl.LoadOK)
                        Debug.WriteLine(jrl.LoadErrorMessage);
                        JJUnLoadAppDomain();
                        return;
                    // 调用方法
                    object retstr = jrl.Invoke("testa命名空间.MyCommands类", "gggg");
                catch (System.Exception exception)
                    Debugger.Break();
                    Debug.WriteLine(exception.Message);
                CmdUnknown();
    

    反射导出cad命令

    using System.Collections.Generic;
    using System.Reflection;
    using System;
    using System.IO;
    using Autodesk.AutoCAD.Runtime;
    namespace JoinBox
        [Serializable]
        public class LoadAcadCmds
            public Dictionary<string, LoadAcadCmdsInfo> AcadDllInfos { get; set; }
            /// <summary>
            /// 反射导出Acad的注册的命令
            /// </summary>
            /// <param name="dllFileNames">Acad注册的命令的Dll</param>
            /// <returns></returns>
            public LoadAcadCmds(Assembly ass)
                AcadDllInfos = new();
                var tyeps = new Type[] { };
                    //获取类型集合,反射时候还依赖其他的dll就会这个错误
                    tyeps = ass?.GetTypes();
                catch (ReflectionTypeLoadException)//反射时候还依赖其他的dll就会这个错误
                foreach (var type in tyeps)
                    if (!type.IsClass || !type.IsPublic)
                        continue;
                    foreach (MethodInfo method in type.GetMethods())
                        if (!(method.IsPublic && method.GetCustomAttributes(true).Length > 0))
                            continue;
                        CommandMethodAttribute cadAtt = null;
                        foreach (var att in method.GetCustomAttributes(true))
                            var name = att.GetType()?.Name;
                            if (name == typeof(CommandMethodAttribute).Name)
                                cadAtt = att as CommandMethodAttribute;
                        if (cadAtt == null)
                            continue;
                        var dllName = Path.GetFileNameWithoutExtension(
                                   ass.ManifestModule.Name.Substring(0, ass.ManifestModule.Name.Length - 4));
                        var cmdup = cadAtt.GlobalName.ToUpper();
                        var info = new LoadAcadCmdsInfo
                            DllName = dllName,//不一定有文件名
                            Namespace = type.Namespace,
                            ClassName = type.Name,
                            CmdName = cmdup,
                            MethodName = method.Name,
                        //将cad命令作为key进行索引
                        if (!AcadDllInfos.ContainsKey(cmdup))
                            AcadDllInfos.Add(cmdup, info);
        /// <summary>
        /// 提取cad命令类信息
        /// </summary>
        [Serializable]
        public class LoadAcadCmdsInfo
            public string DllName;
            public string Namespace;
            public string ClassName;
            public string CmdName;
            public string MethodName;
    

    通讯类dll工程

    创建程序域和初始化通讯类JAppDomain

    using System;
    using System.IO;
    using System.Reflection;
    namespace RemoteAccess
        public class JAppDomain : IDisposable
            /// <summary>
            /// 新域
            /// </summary>
            AppDomain _newAppDomain;
            /// <summary>
            /// 新域的通讯代理类(通过它和其他程序域沟通)
            /// </summary>
            public RemoteLoader JRemoteLoader { get; set; }
            /// <summary>
            /// 程序域的创建和释放
            /// </summary>
            /// <param name="newAppDomainName">新程序域名</param>
            /// <param name="assemblyPlugs">子目录(相对形式)在AppDomainSetup中加入外部程序集的所在目录,多个目录用分号间隔</param>
            public JAppDomain(string newAppDomainName, string assemblyPlugs = null)
                // 如果是文件就转为路径
                var ap = assemblyPlugs;
                if (!string.IsNullOrEmpty(ap))
                    ap = Path.GetDirectoryName(ap);
                var path = RemoteLoaderTool.GetAssemblyPath(true); //插件目录
                path = Path.GetDirectoryName(path);
                if (!string.IsNullOrEmpty(ap) && ap != path)
                    ap += ";" + path;
                // 创建App域
                var ads = new AppDomainSetup
                    ApplicationName = newAppDomainName,
                    // 应用程序根目录
                    ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
                    // 子目录(相对形式)在AppDomainSetup中加入外部程序集的所在目录,多个目录用分号间隔
                    PrivateBinPath = ap ??= path,
                //设置缓存目录
                ads.CachePath = ads.ApplicationBase;   //获取或设置指示影像复制是打开还是关闭
                ads.ShadowCopyFiles = "true";          //获取或设置目录的名称,这些目录包含要影像复制的程序集
                ads.ShadowCopyDirectories = ads.ApplicationBase;
                ads.DisallowBindingRedirects = false;
                ads.DisallowCodeDownload = true;
                ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
    #if true4
                //从安全策略证据新建程序域(应该是这句导致通讯类无法获取文件)
                var adevidence = AppDomain.CurrentDomain.Evidence;
                // 创建第二个应用程序域。
                AppDomainFactory.JAppDomain = AppDomain.CreateDomain(newAppDomainName, adevidence, ads);
    #endif
                _newAppDomain = AppDomain.CreateDomain(newAppDomainName, null, ads);
                // 遍历程序集,获取指定的程序集 RemoteLoader
                string assemblyName = null;
                foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies())
                    if (ass == typeof(RemoteLoader).Assembly)
                        assemblyName = ass.FullName;
                        break;
                if (assemblyName == null)
                    throw new ArgumentNullException(nameof(RemoteLoader) + "程序域不存在");
    #if true2
                    var masterAppDomain = AppDomain.CurrentDomain;
                    // 如果通讯类是主域的话表示新域没用,加载进来的东西都在主域.
                    // 做了个寂寞,释放了空域
                    JRemoteLoader = masterAppDomain.CreateInstanceAndUnwrap(assemblyName, typeof(RemoteLoader).FullName) as RemoteLoader;
    #else
                    // 在新域创建通讯类,会引发错误: System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
                    // 因为使用了cad的dll,而它的dll用了非托管对象
                    JRemoteLoader = _newAppDomain.CreateInstanceAndUnwrap(assemblyName, typeof(RemoteLoader).FullName) as RemoteLoader;
    #endif
                catch (Exception)
                    AppDomain.Unload(_newAppDomain);
                    _newAppDomain = null;
                    JRemoteLoader = null;
                    throw new ArgumentNullException(
                        "需要将*通讯类.dll*扔到acad.exe,而c盘权限太高了," +
                        "所以直接复制一份acad.exe所有文件到你的主工程Debug目录," +
                        "调试都改到这个目录上面的acad.exe");
                // 不用下面的字符串形式,否则改个名就报错了...
                //try
                //    JRemoteLoader = _newAppDomain.CreateInstance(
                //           RemoteLoaderTool.RemoteAccessNamespace,
                //           RemoteLoaderTool.RemoteAccessNamespace + ".RemoteLoader")//类名
                //           .Unwrap() as RemoteLoader;
                //catch (Exception e)//报错是否改了 RemoteLoader名称
                //    throw e;
            #region Dispose
            public bool Disposed = false;
            /// <summary>
            /// 显式调用Dispose方法,继承IDisposable
            /// </summary>
            public void Dispose()
                //由手动释放
                Dispose(true);
                //通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
                GC.SuppressFinalize(this);
            /// <summary>
            /// 析构函数,以备忘记了显式调用Dispose方法
            /// </summary>
            ~JAppDomain()
                Dispose(false);//由系统释放
            /// <summary>
            /// 释放
            /// </summary>
            /// <param name="ing"></param>
            protected virtual void Dispose(bool ing)
                if (Disposed) return; //不重复释放
                Disposed = true;//让类型知道自己已经被释放
                // 系统卸载出错,而手动卸载没出错,因为要留意JRemoteLoader对象在什么域的什么对象上.
                if (_newAppDomain != null)
                    JRemoteLoader?.Dispose();
                    JRemoteLoader = null;
                    AppDomain.Unload(_newAppDomain);
                _newAppDomain = null;
                GC.Collect();
            #endregion
    

    通讯类RemoteLoader

    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Runtime.Remoting;
    //参考文章1 https://www.cnblogs.com/zlgcool/archive/2008/10/12/1309616.html
    //参考文章2 https://www.cnblogs.com/roucheng/p/csdongtai.html
    namespace RemoteAccess
        [Serializable]
        public class AssemblyInfo
            public Assembly Assembly;
            public string Namespace;
            public string Class;
            public string Method;
            public string TypeFullName;
        /// <summary>
        /// 通讯代理类
        /// </summary>
        [Serializable]
        public class RemoteLoader : MarshalByRefObject, IDisposable
            #region 成员
            AssemblyDependent AssemblyDependent;
            /// <summary>
            /// 链条头的dll加载成功
            /// </summary>
            public bool LoadOK { get; set; }
            /// <summary>
            /// 加载的错误信息,可以获取到链条中段的错误
            /// </summary>
            public string LoadErrorMessage { get; set; }
            #endregion
            private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
            public RemoteLoader() { }
            /// <summary>
            /// 域内进行链式加载dll
            /// </summary>
            /// <param name="file"></param>
            public void LoadAssembly(string sFile)
                AssemblyDependent = new AssemblyDependent(sFile);
                AssemblyDependent.CurrentDomainAssemblyResolveEvent +=
                    RunTimeCurrentDomain.DefaultAssemblyResolve; //运行域事件保证跨dll的搜索
               AssemblyDependent.Load();
               LoadOK = AssemblyDependent.LoadOK;
               LoadErrorMessage = AssemblyDependent.LoadErrorMessage;
            /// <summary>
            /// 加载cad的东西只能在外面做,
            /// 而且远程通讯方法又必须在MarshalByRefObject接口下,
            /// 所以这提供遍历加载的程序集们方法
            /// </summary>
            /// <param name="ac"><see cref="RemoteLoader"/>封送本类|
            /// <see cref="Assembly"/>封送程序集|
            /// <see cref="object[]"/>封送参数|
            /// <see cref="object"/>封送返回值,<see cref="!null"/>结束循环.
            /// </param>
            /// <param name="ars">外参数传入封送接口</param>
            public object TraversideLoadAssemblys(Func<RemoteLoader, Assembly, object[], object> ac, object[] ars = null)
                object value = null;
                if (AssemblyDependent == null)
                    return value;
                foreach (var assembly in AssemblyDependent.MyLoadAssemblys)
                    value = ac.Invoke(this, assembly, ars);
                    if (value != null)
                        break;
                return value;
            /// <summary>
            /// 调用载入的dll的方法
            /// </summary>
            /// <param name="spaceClass">命名空间+类名</param>
            /// <param name="methodName">方法名</param>
            /// <param name="args">方法需要的参数</param>
            /// <returns></returns>
            public object Invoke(string spaceClass, string methodName, params object[] args)
                if (AssemblyDependent.MyLoadAssemblys.Count < 1)
                    throw new ArgumentNullException("没构造或加载失败");
                var assemblyInfo = GetAssembly(spaceClass, methodName);
                if (assemblyInfo == null)
                    throw new NotSupportedException("找不到指定的命名空间和类名:" + spaceClass);
                return Invoke(assemblyInfo.Assembly, assemblyInfo.Namespace + "." + assemblyInfo.Class, assemblyInfo.Method, args);
            /// <summary>
            /// 遍历出"方法"在链条中什么dll上,返回程序集
            /// </summary>
            /// <param name="className">命名空间+类名</param>
            /// <param name="methodName">方法名</param>
            /// <param name="typeFullName">返回类型名</param>
            /// <returns>程序集</returns>
            AssemblyInfo GetAssembly(string className, string methodName)
                var ta = this.TraversideLoadAssemblys((remoteLoaderFactory, assembly, ars) => {
                    //获取所有类型
                    Type[] types = assembly.GetTypes();
                    foreach (var type in types)
                        if (!type.IsClass || !type.IsPublic)
                            continue;
                        if (type.Namespace + "." + type.Name != className)
                            continue;
                        foreach (MethodInfo method in type.GetMethods())
                            if (!method.IsPublic)
                                continue;
                            // cad可以用这个,属性名称
                            //if (method.GetCustomAttributes(true).Length == 0)
                            //    continue;
                            // 转大写匹配命令
                            if (method.Name.ToUpper() == methodName.ToUpper())
                                var asInfo = new AssemblyInfo
                                    TypeFullName = type.FullName,
                                    Assembly = assembly,
                                    Namespace = type.Namespace,
                                    Class = type.Name,
                                    Method = method.Name,
                                return asInfo;
                    return null;
                return (AssemblyInfo)ta;
            public object Invoke(Assembly assembly, string spaceClass, string methodName, params object[] args)
                if (assembly == null)
                    throw new ArgumentNullException("没程序域");
                Type spaceClassType = assembly.GetType(spaceClass);
                if (spaceClassType == null)
                    throw new ArgumentNullException("命名空间.类型出错");
                // 转大写匹配命令(如果是方法的话,这里可能有重载)
                List<MethodInfo> methodInfos = new();
                foreach (var item in spaceClassType.GetMethods())
                    if (item.Name.ToUpper() == methodName.ToUpper())
                        methodInfos.Add(item);
                if (methodInfos.Count == 0)
                    throw new ArgumentNullException("方法出错");
                object spaceClassInstance = Activator.CreateInstance(spaceClassType);
                object returnValue = null;
                foreach (var methodInfo in methodInfos)
                        // 此句若出错表示运行域不在准确的域内,要去检查一下,此句也会导致GC释放出错
                        // 没有参数
                        returnValue = methodInfo.Invoke(spaceClassInstance, args);
                        // 参数1,重载
                        // returnValue = methodInfo.Invoke(spaceClassInstance, new object[] { "fsdfasfasf" });
                    catch
                // 调用方式改变(但是这个方法需要 Assembly.LoadForm 否则无法查找影像文件)
                // return Activator.CreateInstanceFrom(assembly.FullName, spaceClass + "." + methodName, false, bfi, null, args, null, null, null).Unwrap();
                return returnValue;
            #region Dispose
            public bool Disposed = false;
            /// <summary>
            /// 显式调用Dispose方法,继承IDisposable
            /// </summary>
            public void Dispose()
                //由手动释放
                Dispose(true);
                //通知垃圾回收机制不再调用终结器(析构器)_跑了这里就不会跑析构函数了
                GC.SuppressFinalize(this);
            /// <summary>
            /// 析构函数,以备忘记了显式调用Dispose方法
            /// </summary>
            ~RemoteLoader()
                Dispose(false);//由系统释放
            /// <summary>
            /// 释放
            /// </summary>
            /// <param name="ing"></param>
            protected virtual void Dispose(bool ing)
                if (Disposed) return;//不重复释放            
                Disposed = true;//让类型知道自己已经被释放
                if (AssemblyDependent == null)  return;              
                AssemblyDependent.CurrentDomainAssemblyResolveEvent -=
                  RunTimeCurrentDomain.DefaultAssemblyResolve; //运行域事件保证跨dll的搜索
            #endregion
    

    通讯类RemoteLoader其余

  • 本文的 cad主插件项目 - 链式加载
  • 本文的 cad主插件项目 - 运行域事件
  •