cad.net dll动态加载和卸载

20250320
不应该loadx时候采用链式加载,而是loadx命令只加载目标dll,
每个加载的dll应该独立程序域.
如果加入了同名不同版本的,就可以自动卸载之前版本的.
当触发需要之前的版本时候,通过事件加载回来.

我实现在此处:
插件式架构最小实现:
https://www.cnblogs.com/JJBox/p/18800009

下面文章为旧文,内容存在错误,
例如我把Assembly类传输了,这是一个错误,跨域只能传输值类型,
请用上文最小实现.
_
_
_
_
_
_
_

需求

1: 我们cad.net开发都会面临一个问题,
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重载,
也就无法拷贝到内存中去,故此不考虑.

img

南胜写了一篇文章回答了,
但是只支持一个引用的dll,而我需要知道引用的引用...的dll.
所以对他的代码修改一番.

工程开始

项目地址
目前博客比较新

首先,共有四个项目.

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

cad子插件项目

// testa项目代码
namespace LoadTesta {
    public class MyCommands {
        [CommandMethod(nameof(testa))]
        public static void testa() {
            var doc = Acap.DocumentManager.MdiActiveDocument;
            if (doc is null) return;
            doc.Editor.WriteMessage("\r\n 自带函数" + mameof(testa));
        }

        [CommandMethod(nameof(gggg))]
        public void gggg()  {
            var doc = Acap.DocumentManager.MdiActiveDocument;
            if (doc is null) return;
            doc.Editor.WriteMessage("\r\n ****" + nameof(gggg));  
            testb.MyCommands.TestBHello();
        }
    }
}


// testb项目代码
namespace LoadTestb {
    public class MyCommands {
        public static void TestBHello() {
            var doc = Acap.DocumentManager.MdiActiveDocument;
            if (doc is null) return;
            doc.Editor.WriteMessage("\r\n ****" + nameof(TestBHello));
            testc.MyCommands.TestcHello();   
        }

        [CommandMethod(nameof(testb))]
        public static void testb() {
            var doc = Acap.DocumentManager.MdiActiveDocument;
            if (doc is null) return;
            doc.Editor.WriteMessage("\r\n 自带函数" + nameof(testb));
        }
    }
}


// testc项目代码
namespace LoadTestc {
    public class MyCommands {
        public static void TestCHello() {
            var doc = Acap.DocumentManager.MdiActiveDocument;
            if (doc is null) return;
            doc.Editor.WriteMessage("\r\n ****" + nameof(TestCHello));
        }

        [CommandMethod(nameof(testc))]
        public static void testc() {
            var doc = Acap.DocumentManager.MdiActiveDocument;
            if (doc is null) return;
            doc.Editor.WriteMessage("\r\n 自带函数" + nameof(testc));
        }
    }
}

迭代版本号

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

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

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

然后修改AssemblyInfo.cs
img

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

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

cad主插件项目

概念
先说一下测试环境和概念,
cad主插件上面写一个命令,
调用了WinForm窗体让它接受拖拽dll文件,获取dll路径加载.
也可以简化,做一个loadx命令,获取dll路径加载.

这个时候需要直接启动cad,
然后调用netload命令加载cad主插件的dll.
如果采用vs调试cad启动的话还是会出错.

经过若海两天的Debug发现了: 不能在vs调试状态下运行cad!应该直接启动它!
猜想:这个时候令vs令所有 Assembly.Load(byte) 都进入了vs内存上面,
vs自动占用到 obj\Debug 文件夹下的dll.
我开了个新文章写这个问题

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

WinForm窗体拖拽这个函数网络搜一下基本能搞定,我就不贴代码了,
接收拖拽之后就有个testa.dll的路径,再调用传给加载函数就好了.

获取依赖链并加载

因为此处引用了 nuget 的 Lib.Harmony
所以单独分一个工程出来作为cad工程的引用
免得污染了cad工程的纯洁

#define HarmonyPatch
#define HarmonyPatch_1

namespace IFoxCAD.LoadEx;
#if HarmonyPatch_1
[HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
#endif
public class AssemblyDependent : IEnumerable<Assembly> {
#if HarmonyPatch
    // 这个是不能删除的,否则就不执行了
    // HarmonyPatch hook method 返回 false 表示拦截原函数
    public static bool Prefix() { return false; }
#endif

    /// <summary>
    /// 拦截cad的Loader异常:默认是<paramref name="false"/>
    /// </summary>
    public bool PatchExtensionLoader = false;

    /// <summary>
    /// 当前程序域运行时解析事件,运行时缺失查找加载路径
    /// </summary>
    public event ResolveEventHandler AssemblyResolve {
        add { AppDomain.CurrentDomain.AssemblyResolve += value; }
        remove { AppDomain.CurrentDomain.AssemblyResolve -= value; }
    }

    /// <summary>
    /// 当前程序域仅反射解析事件,运行时缺失查找加载路径
    /// </summary>
    public event ResolveEventHandler ReflectionOnlyAssemblyResolve {
        add { AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += value; }
        remove { AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= value; }
    }

    /// <summary>
    /// 加载成功的程序集
    /// 成功的会被程序域持有
    /// 此处有路径索引,防止卸载时候有强引用造成卸载失败
    /// </summary>
    public Dictionary<string, WeakReference<Assembly>> LoadMap = new(StringComparer.OrdinalIgnoreCase);
    Dictionary<string, string> LoadMapError = new();

    // 迭代器
    public IEnumerator<Assembly> GetEnumerator()
        // 先收集无效键,再移除.
        var invalidKeys = _adep.LoadMap
            .Where(pair => !pair.Value.TryGetTarget(out _))
            .Select(pair => pair.Key)
            .ToList();
        foreach (var key in invalidKeys)
            _adep.LoadMap.Remove(key);

        // 返回剩余的有效程序集
        foreach (var pair in _adep.LoadMap) {
            if (pair.Value.TryGetTarget(out var assembly) && assembly != null)
                yield return assembly;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }

    /// <summary>
    /// 加载程序集
    /// </summary>
    /// <param name="dllFile">dll的文件位置</param>
    /// <param name="byteLoad">true字节加载,false文件加载</param>
    /// <returns>参数加载成功标识<paramref name="dllFile"/></returns>
    [MethodImpl(MethodImplOptions.NoInlining)]
    public bool Load(string dllFile, bool byteLoad = true) {
        if (dllFile is null)
            throw new ArgumentNullException(nameof(dllFile));
        // 相对路径要先转换
        dllFile = Path.GetFullPath(dllFile);
        if (!File.Exists(dllFile))
            throw new ArgumentException("路径不存在");

        // 获取加载链,Location不一定存在
        // FullName: System.ComponentModel.Primitives, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
        // Location: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0\System.ComponentModel.Primitives.dll 

        HashSet<string> dllFileSet = new(StringComparer.OrdinalIgnoreCase);
        var cadAssembly = AppDomain.CurrentDomain.GetAssemblies()
            .ToDictionary(ass => ass.FullName, ass => ass, StringComparer.OrdinalIgnoreCase);
        var cadAssemblyRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies()
            .ToDictionary(ass => ass.FullName, ass => ass, StringComparer.OrdinalIgnoreCase);
        GetDependencyLink(cadAssembly, cadAssemblyRef, dllFile, dllFileSet);

        // 加载链进行加载
        foreach(var mfile in dllFileSet) {
            // 名称和版本号完全一致,不进行加载
            if (cadAssembly.ContiansKey(AssemblyName.GetAssemblyName(mfile).FullName)) {
                LoadMapError[mfile] = "版本号已经存在";
                continue;
            }
            try {
                var ass = GetPdbAssembly(mfile);
                ass ??= byteLoad
                    ? Assembly.Load(File.ReadAllBytes(mfile))
                    : Assembly.LoadFile(mfile);
                LoadMap[mfile] = ass;
            } catch(Exception ex) { LoadMapError[mfile] = "{ex.Message}"; }
        }
        return LoadMap.ContainsKey(dllFile);
    }

    // 为了实现Debug时候出现断点
    // https://www.cnblogs.com/DasonKwok/p/10510218.html
    // https://www.cnblogs.com/DasonKwok/p/10523279.html
    /// <summary>
    /// 在debug模式的时候才获取PBD调试信息
    /// </summary>
    /// <param name="path"></param>
    /// <param name="byteLoad"></param>
    /// <returns></returns>
    [MethodImpl(MethodImplOptions.NoInlining)]
    [Conditional("DEBUG")]
    Assembly? GetPdbAssembly(string file) {
        var dir = Path.GetDirectoryName(file);
        var pdbName = Path.GetFileNameWithoutExtension(file);
        var pdbFile = Path.Combine(dir, $"{pdbName}.pdb");
        if (!File.Exists(pdbFile)) return null;
        return Assembly.Load(File.ReadAllBytes(file), File.ReadAllBytes(pdbFile));
    }

    /// <summary>
    /// 递归获取加载链
    /// </summary>
    /// <param name="cadAssembly">程序集运行解析</param>
    /// <param name="cadAssemblyRef">程序集反射解析</param>
    /// <param name="dllFile">dll文件位置</param>
    /// <param name="dllFileSet">dll文件集合返回用</param>
    /// <returns></returns>
    [MethodImpl(MethodImplOptions.NoInlining)]
    void GetDependencyLink(
        Dictionary<string, Assembly> cadAssembly,
        Dictionary<string, Assembly> cadAssemblyRef,
        string dllFile, HashSet<string> dllFileSet) {
        if (dllFile is null)
            throw new ArgumentNullException(nameof(dllFile));

        if (!File.Exists(dllFile)) return;
        if (!dllFileSet.Add(dllFile)) return;

        // 路径转程序集名
        var assName = AssemblyName.GetAssemblyName(dllFile).FullName;

        Assembly assRef;
        // 运解和反解都没有,
        // 就把dll加载到反解以及更新缓存,用来找依赖路径,
        if (!cadAssembly.TryGetValue(assName, out assRef)) {
            if (!cadAssemblyRef.TryGetValue(assName, out assRef)) {
                assRef = TryOnlyLoad(dllFile);
                if (assRef is not null) {
                    if (assName != assRef.FullName) throw new("理论上它们是一样");
                    cadAssemblyRef[assRef.FullName] = assRef;
                }
            }
        }
        if (assRef is null) return;

        // 递归获取依赖路径
        // 拖拽加载dll的目录,过滤得到为程序集名的dll.
        var dir = Path.GetDirectoryName(dllFile);
        var files = assRef.GetReferencedAssemblies()
            .Select(ass => Path.Combine(dir, $"{ass.Name}.dll"))
            .Where(file => !dllFileSet.Contains(file) && File.Exists(file))
            .ToArray();
        foreach(var file in files) {
            GetDependencyLink(cadAssembly, cadAssemblyRef, file, dllFileSet);
        }
    }

    /// <summary>
    /// 反射解析加载文件
    /// </summary>
    /// <param name="dllFile">dll文件位置</param>
    /// <returns>反射解析内的程序集</returns>
    [MethodImpl(MethodImplOptions.NoInlining)]
    Assembly? TryOnlyLoad(string dllFile) {
        try {
        var byteRef = File.ReadAllBytes(dllFile);
        // 若用名称参数,没有依赖会报错,所以用字节参数.
        // 不能重复加载同一个dll,用hashset排除.
        if (!PatchExtensionLoader) {
            return Assembly.ReflectionOnlyLoad(byteRef);
        }

#if HarmonyPatch_1
        // QQ1548253108: 我这里会报错,提供了解决方案.
        // 方案一: 在类上面加 [HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
        const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
        Harmony hm = new(ext);
        hm.PatchAll();
        result = 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"));
        result = Assembly.ReflectionOnlyLoad(byteRef);
        hm.UnpatchAll(ext);
#endif
        return result;
        } catch { return null; }
    }

    /// <summary>
    /// 加载信息
    /// </summary>
    public string PrintMessage() {
        var sb = new StringBuilder();
        foreach (var pair in LoadMap) {
            sb.AppendLine($"++  {pair.Key}");
        }
        if (sb.Count > 0) 
            sb.Insert(0, "**  这些文件加载成功:");

        int num = sb.Length;
        foreach (var pair in LoadMapError) {
            sb.AppendLine($"--  {pair.Key},  错误消息:{pair.Value}");
        }
        if (sb.Length > num) 
            sb.Insert(num, "**  这些文件已被加载过,同时重复名称和版本号,跳过:");
        return sb.ToString();
    }
}

运行域事件

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

关于AppDomain.CurrentDomain.AssemblyResolve事件,
它用于在程序集加载失败时重新加载程序集,
通常用于动态加载程序集的场景,
你可以通过该事件自定义程序集的查找和加载逻辑,确保程序集能够正确加载.

0x01 动态加载要注意所有的引用外的dll的加载顺序
0x02 指定版本: Acad2008若没有这个事件.
1,动态编译的命令执行时候无法引用当前的程序集函数,弹出一个报错.
2,动态编译之后要加载程序域内,就能实现触发默认检索机制,不需要这个事件.
所以这是两种方案,事件进行动态检索可以动态更换程序域顺序,从而更换dll.
0x03 目录构成: 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
0x04 命令构成: cad自带的netload会把所处的运行域给改到cad自己的,而动态加载不通过netload,所以要自己去改.

调用

AppDomain.CurrentDomain.AssemblyResolve += AssemblyHelper.AssemblyResolve;
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += AssemblyHelper.ReflectionOnlyAssemblyResolve;

封装

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.IO;
using System.Linq;
using System.Configuration;

namespace IFoxCAD.LoadEx;

public class AssemblyHelper {
    // 仅反射加载时解析程序集,不触发静态构造函数等
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static Assembly? ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs e)
    {
        var cadAss = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
        return Resolve(cadAss, sender, e);
    }

    // 常规程序集解析失败处理
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static Assembly? AssemblyResolve(object sender, ResolveEventArgs e)
    {
        var cadAss = AppDomain.CurrentDomain.GetAssemblies();
        return Resolve(cadAss, sender, e);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static Assembly? Resolve(Assembly[] cadAss, object sender, ResolveEventArgs e)
    {
        // 精确匹配全名(名称/版本/公钥等)
        var exactMatch = cadAss.FirstOrDefault(ass => e.Name == ass.FullName);
        if (exactMatch != null) return exactMatch;

        // 解析请求的程序集基本信息
        var requestedName = new AssemblyName(e.Name);

        // 模糊匹配: 名称相同且版本最高的程序集
        var highestVersionMatch = cadAss
            .Where(ass => ass.GetName().Name == requestedName.Name)
            .OrderByDescending(ass => ass.GetName().Version)
            .FirstOrDefault();

        if (highestVersionMatch != null) return highestVersionMatch;

        // 尝试在文件系统中查找程序集
        string? assemblyPath = FindAssemblyPath(e.Name);
        if (!string.IsNullOrEmpty(assemblyPath) && File.Exists(assemblyPath))
            return Assembly.LoadFrom(assemblyPath);

        // 处理资源程序集找不到的情况(参考:https://stackoverflow.com/a/43818961)
        if (requestedName.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)
            && requestedName.CultureInfo?.IsNeutralCulture == false)
            return null;

        // 记录详细的错误信息
        var errorLog = new StringBuilder();
        errorLog.AppendLine($"程序集解析失败 [{DateTime.Now:yyyy-MM-dd HH:mm:ss}]");
        errorLog.AppendLine($"请求的程序集全名: {e.Name}");
        errorLog.AppendLine($"已加载的程序集列表({cadAss.Length}个):");
        foreach (var ass in cadAss.OrderBy(a => a.FullName))
            errorLog.AppendLine($"  - {ass.FullName}");

        Trace.TraceError(errorLog.ToString());

        // 调试时中断,发布环境建议记录日志后终止
        Debugger.Break();
        return null;
    }


    private static string? FindAssemblyPath(string assemblyFullName) {
        var requestedName = new AssemblyName(assemblyFullName);
        string[] searchPaths = {
            AppDomain.CurrentDomain.BaseDirectory,
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"),
            ConfigurationManager.AppSettings["CustomAssemblyPath"],
            @"C:\Program Files\IFoxCAD\Assemblies"
        };

        // 查找程序集路径
        var foundPath = searchPaths
            .Where(Directory.Exists) // 过滤存在的目录
            .Select(dir => Path.Combine(dir, $"{requestedName.Name}.dll")) // 生成目标路径
            .FirstOrDefault(File.Exists); // 返回第一个存在的文件路径

        // 如果找到路径,直接返回
        if (foundPath != null) return foundPath;

        // WPF 程序集特殊处理(调试时使用本地版本)
        if (Debugger.IsAttached && requestedName.Name.StartsWith("PresentationCore")) {
            string wpfLocation = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
                @"Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8",
                $"{requestedName.Name}.dll");

            if (File.Exists(wpfLocation)) return wpfLocation;
        }

        // 如果都找不到
        return null;
    }


    // 从嵌入资源加载程序集(适用于单文件发布)
    private static Assembly? OnResolveAssembly(object sender, ResolveEventArgs args)
    {
        var resourceName = $"{new AssemblyName(args.Name).Name}.dll";

        // 精确匹配资源名称(考虑命名空间路径)
        var fullResourceName = Assembly.GetExecutingAssembly()
            .GetManifestResourceNames()
            .FirstOrDefault(name => name.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase));

        if (fullResourceName is null) return null;

        using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fullResourceName);
        if (stream is null) return null;

        byte[] buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        return Assembly.Load(buffer);
    }
}
        // Acad21+vs22 容易触发这个资源的问题
        // https://stackoverflow.com/questions/4368201
        // if (an.Name.EndsWith(".resources") && !an.CultureName.EndsWith("neutral"))
        //     return null;
        // Process.GetCurrentProcess().Kill();

动态编译

using System;
using System.CodeDom.Compiler;
using Microsoft.CSharp;

public class DynamicCompiler {
    public static void Main() {
        // 动态编译代码
        string code = @"
            using System;
            public class HelloWorld {
                public static void SayHello() {
                    Console.WriteLine(""Hello, World!"");
                }
            }";

        var provider = new CSharpCodeProvider();
        var compilerParams = new CompilerParameters
        {
            GenerateInMemory = true, // 在内存中生成程序集
            ReferencedAssemblies = { "System.dll" } // 添加引用
        };

        CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, code);

        if (results.Errors.HasErrors)
        {
            foreach (var error in results.Errors)
                Console.WriteLine(error.ToString());
            return;
        }

        // 显式加载生成的程序集,就不用使用事件了.
        var compiledAssembly = results.CompiledAssembly;
        AppDomain.CurrentDomain.Load(compiledAssembly.GetName());

        // 调用动态编译的方法
        var type = compiledAssembly.GetType("HelloWorld");
        var method = type.GetMethod("SayHello");
        method.Invoke(null, null);
    }
}

新程序域并执行代码

// 可跨应用程序域调用的类
public class RemoteObject : MarshalByRefObject {
    public void SayHello() {
        Console.WriteLine("Hello from " + AppDomain.CurrentDomain.FriendlyName);
    }
}

class Program {
    static void Main() {
        // 创建新的应用程序域
        AppDomain newDomain = AppDomain.CreateDomain("NewDomain");

        // 在新的应用程序域中执行代码
        newDomain.DoCallBack(() => {
            Console.WriteLine("Executing in new AppDomain: " + AppDomain.CurrentDomain.FriendlyName);
        });

        // 在新的应用程序域中创建对象
        RemoteObject obj = (RemoteObject)newDomain.CreateInstanceAndUnwrap(
            typeof(RemoteObject).Assembly.FullName,
            typeof(RemoteObject).FullName);

        // 调用方法
        obj.SayHello();

        // 卸载应用程序域
        AppDomain.Unload(newDomain);
    }
}

新域内切换上下文:
这本质上还是跨域传递,需要序列化特性才行.

using System;
using System.Runtime.Remoting.Messaging;

class Program {
    static void Main() {
        // 设置上下文数据
        CallContext.LogicalSetData("MyKey", "Hello from Main Context!");

        // 获取上下文数据
        var data = CallContext.LogicalGetData("MyKey");
        Console.WriteLine("Main Context Data: " + data);

        // 创建一个新的上下文
        using (new MyContext()) {
            // 在新上下文中设置数据
            CallContext.LogicalSetData("MyKey", "Hello from New Context!");

            // 获取新上下文中的数据
            var newData = CallContext.LogicalGetData("MyKey");
            Console.WriteLine("New Context Data: " + newData);
        }

        // 回到主上下文后,检查数据
        var originalData = CallContext.LogicalGetData("MyKey");
        Console.WriteLine("Back to Main Context Data: " + originalData);
    }
}

// 自定义上下文类
public class MyContext : IDisposable {
    private readonly object _oldContext;
    public MyContext() {
        // 保存当前上下文
        _oldContext = CallContext.LogicalGetData("MyKey");
        // 设置新的上下文数据
        CallContext.LogicalSetData("MyKey", "Initialized in New Context");
        Console.WriteLine("New Context Created.");
    }
    public void Dispose() {
        // 恢复原来的上下文数据
        CallContext.LogicalSetData("MyKey", _oldContext);
        Console.WriteLine("Context Restored.");
    }
}

创建程序域两个函数是不同的
CreateInstance
CreateInstanceAndUnwrap

using System;
using System.IO;
using System.Reflection;

class Program {
    static void Main() {
        // 读取DLL到字节数组
        byte[] dllBytes = File.ReadAllBytes("YourDll.dll"); // 替换为实际路径

        // ----------- 主程序域加载 -----------
        Console.WriteLine("【主程序域加载】");
        Assembly mainAssembly = Assembly.Load(dllBytes); // 字节流加载[1](@ref)
        Type mainType = mainAssembly.GetType("Namespace.ClassName"); // 替换为实际类名
        dynamic mainInstance = Activator.CreateInstance(mainType);
        mainInstance.YourMethod(); // 调用目标方法

        // ----------- 新域加载 -----------
        Console.WriteLine("\n【新域加载】");
        AppDomain newDomain = AppDomain.CreateDomain("NewDomain");
        try
        {
            // 使用Loader类实现跨域字节流加载
            ObjectHandle loaderHandle = newDomain.CreateInstance(
                typeof(CrossDomainLoader).Assembly.FullName,
                typeof(CrossDomainLoader).FullName
            );
            
            CrossDomainLoader loader = (CrossDomainLoader)loaderHandle.Unwrap();
            loader.LoadAndExecute(dllBytes, "Namespace.ClassName", "YourMethod");
        }
        finally
        {
            AppDomain.Unload(newDomain); // 卸载新域[2](@ref)
        }
    }
}

// 跨域加载辅助类(需继承MarshalByRefObject)
public class CrossDomainLoader : MarshalByRefObject
{
    public void LoadAndExecute(byte[] dllBytes, string className, string methodName)
    {
        Assembly assembly = Assembly.Load(dllBytes); // 在新域中加载[3](@ref)
        Type type = assembly.GetType(className);
        dynamic instance = Activator.CreateInstance(type);
        instance.GetType().GetMethod(methodName).Invoke(instance, null);
    }
}

调试

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

卸载DLL

项目结构

  1. cad主插件工程,引用-->通讯类工程
  2. 通讯类工程(继承MarshalByRefObject接口的)
  3. 其他需要加载的子插件工程: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主插件工程-->主域-->新域-->创建域-->创建通讯类-->通讯类 通讯类-->执行链式加载子插件工程-->新域程序集 通讯类-->新域程序集 新域-->卸载域-->破坏通讯类-->通讯类 主域-->跨域执行方法-->检查通讯类生命-->通讯类

当时我搞卸载的时候怎么没看到下面链接,
新版net出来微软就弄了?拒绝内联之类的,我net3.5怎么阻止.
卸载的链接参考

acad卸载程序域是成功的,
但是存在COM的GCHandle问题:
在新域上面创建通讯类--卸载成功,不过关闭cad程序的时候弹出了报错:
System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”

我查询这个错误,大部分是讲COM的,而我工程上面刚好使用了COM,
所以最好就是检测是不是有静态字段持有COM,最好每次使用就执行释放,不要静态持有.
甚至怀疑是cad的Acap的文档集合本身就会创建COM导致异常...

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

旧工程:
Loadx分支呈现旧代码

最新工程:
https://www.cnblogs.com/JJBox/p/18800009

(完)

posted @ 2020-10-18 03:58  惊惊  阅读(10189)  评论(2)    收藏  举报