cad.net AOP注入代码

AOP

几个包说明:
Mono.Cecil 它不能动态注入,只可以选择一个dll然后注入.
Fody 能反射自己的特性来进行注入
Harmony2 能反射人家并且动态注入,
但是我发现被注入的函数不能断点了,而注入的头尾两个函数可以,真奇怪...估计是注入行为已经破坏了调试文件,但是它没有把调试文件也顺带处理...

另见此文
若你的dll有签名的话,注意实践起来的问题.

Mono.Cecil

引用nuget

<ItemGroup Condition="'$(TargetFramework)' != 'net35'">
   <PackageReference Include="Mono.Cecil" Version="0.11.4" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net35'">
   <PackageReference Include="Mono.Cecil" Version="0.9.4" />
</ItemGroup>

代码

using Autodesk.AutoCAD.Runtime;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;

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

/*
 *  警告: 此处测试若同时使用 nuget:Mono.Cecil 和 0Harmony.dll 将导致冲突
 *        此处为 nuget:Mono.Cecil 的测试
 */
using AssemblyDef = Mono.Cecil.AssemblyDefinition;
using OpCodes = Mono.Cecil.Cil.OpCodes;//0Harmony.dll 把这个类设置为内部的!

/*
 * 旧例子
 * https://www.cnblogs.com/RicCC/archive/2010/03/21/mono-cecil.html
 * https://www.cnblogs.com/whitewolf/archive/2011/07/28/2119969.html
 * 新例子:
 * https://blog.csdn.net/ZslLoveMiwa/article/details/82192522
 * https://blog.csdn.net/lee576/article/details/38780889
 * AssemblyFactory 0.6版本被移除,改用 AssemblyDefinition
 * CilWorker       类被移除,改用 GetILProcessor()
 * Import 过时,改用 ImportReference
 * 
 */

namespace JoinBox
{
    public class TestMonoClass
    {
        [CommandMethod("TestMono")]
        public void TestMono()
        {
            var dm = Acap.DocumentManager;
            var doc = dm.MdiActiveDocument;
            var ed = doc.Editor;
            ed.WriteMessage("Hello, World!");
        }
    }

    public class Program
    {
        [JoinBoxInitialize()]
        public static void Main(string[] args)
        {
            string fullName = AutoGo.ConfigPathAll;
            if (fullName == null || !File.Exists(fullName))
            {
                Console.WriteLine("没有此路径文件:" + fullName);
                return;
            }

            //Mono.Cecil 只能读取非占用文件来改函数,所以用改用 Lib.Harmony
            //没有文件流就不可以保存
            var file = new FileStream(fullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            var assembly = AssemblyDef.ReadAssembly(file);
            if (assembly == null)
                return;

            //构造Console方法
            var tt = typeof(System.Diagnostics.Debug).GetMethod("WriteLine", new Type[] { typeof(string) });

            foreach (var type in assembly.MainModule.Types)
            {
                if (type.Name == "<Module>")
                    continue;

                foreach (var method in type.Methods)
                {
                    if (method.Name != nameof(TestMonoClass.TestMono))//函数名
                        continue;
                    var worker = method.Body.GetILProcessor();

                    //开头插入
                    var ins = method.Body.Instructions[0];
                    //定义字符串变量
                    worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr,
                                                          "Method start…"));

                    //定义方法
                    worker.InsertBefore(ins, worker.Create(OpCodes.Call,
                                                          Helper.ImRef(assembly.MainModule, tt)));

                    //尾巴插入
                    ins = method.Body.Instructions[method.Body.Instructions.Count - 1];
                    //定义字符串变量
                    worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr,
                                                          "Method finish…"));
                    //定义方法
                    worker.InsertBefore(ins, worker.Create(OpCodes.Call,
                                                           Helper.ImRef(assembly.MainModule, tt)));
                    break;
                }
            }
            assembly.Write(fullName);
        }
    }
    public static class Helper
    {
        public static Mono.Cecil.MethodReference ImRef(Mono.Cecil.ModuleDefinition module, MethodInfo methodInfo)
        {
#if NET35
            return module.Import(methodInfo);
#else
            return module.ImportReference(methodInfo);
#endif
        }
    }
}

Fody

https://blog.csdn.net/qq_28448587/article/details/120067843
https://www.cnblogs.com/cqgis/p/6360231.html

FodyWeavers.xml 编译的时候自动生成,所以不需要准备

引用nuget

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <Platforms>AnyCPU;x64</Platforms>
  </PropertyGroup>

  <ItemGroup>
        <PackageReference Include="Costura.Fody" Version="5.8.0-alpha0098" PrivateAssets="All" />
        <PackageReference Include="MethodDecorator.Fody" Version="1.1.1" PrivateAssets="All" />
        <!--WPF的属性绑定-->
        <PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="All" />
  </ItemGroup>
</Project>

属性注入(WPF用得多)

实现了之后,Fody它会自动找INotifyPropertyChanged接口,然后把这个类下的属性上注入到通知接口

代码

public class Person : INotifyPropertyChanged
{
    //实现事件来进行通知
    public event PropertyChangedEventHandler PropertyChanged;

    //不触发事件通知
    [PropertyChanged.DoNotNotify]
    public string GivenNames { get; set; }

    //触发事件通知
    [PropertyChanged.DependsOn("GivenName", "FamilyName")] //依赖属性
    public string FamilyName { get; set; }

    public string FullName => $"{GivenNames} {FamilyName}";
}

public static void Main(string[] args)
{
    var person = new Person();
    person.PropertyChanged += (sender, e) => {        
        //没有新旧值拦截(修改前,修改后) 有要求就改用:https://github.com/canton7/PropertyChanged.SourceGenerator
        System.Console.WriteLine(sender);//来源 Person
        System.Console.WriteLine(e.PropertyName);//触发通知的属性 FamilyName
    };
    person.FamilyName = "vvvvv";
}

缺陷

没有新旧值拦截(修改前,修改后)

改用: https://github.com/canton7/PropertyChanged.SourceGenerator

函数注入

代码

注册类 AssemblyInfo.cs

//新建一个 AssemblyInfo.cs
using TestDemo;
[module: FodyAtt]
namespace TestDemo
{
    public class AssemblyInfo
    {
    }
}

主函数 Main

public static void Main(string[] args)
{
    //0x02 调用测试类
    var f = new FodyCmd();
    f.DoSomething();
}

测试类 FodyTest.cs

using System;
namespace TestDemo
{
    [FodyAtt]//写在方法上就是方法注入,写在类上就是全部方法注入
    public class FodyCmd
    {
        public FodyCmd()
        {
            //注意,默认构造函数也会执行
            Console.WriteLine("构造函数");
        }

        public void DoSomething()
        {
            Console.WriteLine("执行了:" + nameof(DoSomething));
        }
    }
}

特性

using System;
using System.Reflection;

namespace TestDemo
{
    [AttributeUsage(AttributeTargets.All)]
    public class FodyAtt : Attribute
    {
        protected object InitInstance;
        protected MethodBase InitMethod;
        protected object[] Args;

        public void Init(object instance, MethodBase method, object[] args)
        {
            InitMethod = method;
            InitInstance = instance;
            Args = args;
        }

        public void OnEntry()
        {
            Console.WriteLine("进入函数之前");
        }
        public void OnExit()
        {
            Console.WriteLine("进入函数之后");
        }

        public void OnException(Exception exception)
        {
            Console.WriteLine("例外" + exception.Message);
        }
    }
}

缺陷

Fody很方便,也有不方便的时候

无法使用非自己写的特性进行注入,

或者是我了解不够...因为我没看全API...(我只是个引路人,具体大家深入研究...)

Harmony2

基本上跟fody差不多,但是它可以反射搜索函数,不需要写特性

Demo https://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method
https://harmony.pardeike.net/articles/basics.html

引用nuget

Lib.Harmony

控制台注入例子

官方还有一个例子不是很好懂,因此不展示,上面的链接有.

using HarmonyLib;
using System;

namespace AOP注入测试
{
    public class Program
    {
        static void Main(string[] args)
        {
            var harmony = new Harmony(nameof(Program));
            var mPrefix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddFirst());
            var mPostfix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddLast());
            var mp1 = new HarmonyMethod(mPrefix);
            var mp2 = new HarmonyMethod(mPostfix);

            var mOriginal = AccessTools.Method(typeof(Program), nameof(DD));
            var newMet = harmony.Patch(mOriginal, mp1, mp2);

            DD();
            Console.WriteLine("Main");
        }

        public static void JoinBoxCmdAddFirst()
        {
            Console.WriteLine("JoinBoxCmdAddFirst");
        }

        public static void JoinBoxCmdAddLast()
        {
            Console.WriteLine("JoinBoxCmdAddLast");
        }

        public static void DD()
        {
            Console.WriteLine("Hello World!");//此处无法断点
        } 
    }
}

注入cad命令

它可以反射处理所有的cad命令特性,然后前面注入事务,后面提交事务,中间就不用写事务了.

因为类库用户肯定是想少写一个"事务特性"到命令上面的,
没人想写了几百个命令再去每个命令上补个特性的,
阿惊认为这是代码侵入的方式,非常恶心...因此才放弃fody的注入方式.

而本类库默认情况不侵入用户的命令,
用户想侵入需要手动进行 AOP.Run(nameSpace) 侵入到指定的命名空间,
而启动策略之后,自动将事务侵入命名空间下的命令,此时有拒绝特性的策略保证括免.

所以只是用一个模块就可以改变其他模块行为,实现修改少,作用大.

启用策略

public class AutoAOP
{
    [JoinBoxInitialize]//见 https://www.cnblogs.com/JJBox/p/10850000.html#_label3_0_2_1
    public void Initialize()
    {
        //在所有的命令末尾注入清空事务栈函数
        AOP.Run(nameof(TestNameSpace));
    }
}

注入和反射

using HarmonyLib;

public class JoinBoxRefuseInjectionTransaction : Attribute
{
    /// <summary>
    /// 拒绝注入事务
    /// </summary>
    public JoinBoxRefuseInjectionTransaction()
    {
    }
}
 
public class AOP
{
    /// <summary>
    /// 在此命名空间下的命令末尾注入清空事务栈函数
    /// </summary>
    public static void Run(string nameSpace)
    {
        Dictionary<string, (CommandMethodAttribute Cmd, Type MetType, MethodInfo MetInfo)> cmdDic = new();
        AutoClass.AppDomainGetTypes(type => { //见 https://www.cnblogs.com/JJBox/p/10850000.html
            if (type.Namespace != nameSpace)
                return;
            //类上面特性
            if (type.IsClass)
            {
                var attr = type.GetCustomAttributes(true);
                if (RefuseInjectionTransaction(attr))
                    return;
            }

            //函数上面特性
            var mets = type.GetMethods();//获得它的成员函数
            for (int ii = 0; ii < mets.Length; ii++)
            {
                var method = mets[ii];
                //找到特性,特性下面的方法要是Public,否则就被编译器优化掉了.
                var attr = method.GetCustomAttributes(true);
                for (int jj = 0; jj < attr.Length; jj++)
                    if (attr[jj] is CommandMethodAttribute cmdAtt)
                    {
                        if (!RefuseInjectionTransaction(attr))
                            cmdDic.Add(cmdAtt.GlobalName, (cmdAtt, type, method));
                    }
            }
        });
       
        if (cmdDic.Count == 0)
            return;

        var harmony = new Harmony(nameSpace);
        var mPrefix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddFirst());//进入函数前
        var mPostfix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddLast());//进入函数后
        var mp1 = new HarmonyMethod(mPrefix);
        var mp2 = new HarmonyMethod(mPostfix);

        foreach (var item in cmdDic)
        {
            //原函数执行(所处类type,函数名)
            var mOriginal = AccessTools.Method(item.Value.MetType, item.Value.MetInfo.Name);
            //mOriginal.Invoke();
            //新函数执行:创造两个函数加入里面               
            var newMet = harmony.Patch(mOriginal, mp1, mp2);
            //newMet.Invoke();
        }
    }

    /// <summary>
    /// 含有拒绝注入事务
    /// </summary>
    /// <param name="attr">特性集合</param>
    /// <returns></returns>
    private static bool RefuseInjectionTransaction(object[] attr)
    {
        bool refuseInjectionTransaction = false;
        for (int kk = 0; kk < attr.Length; kk++)
        {
            if (attr[kk] is JoinBoxRefuseInjectionTransaction)
            {
                refuseInjectionTransaction = true;
                break;
            }
        }
        return refuseInjectionTransaction;
    }

    public static Transaction Transaction;
    //命令开启打开事务
    public static void JoinBoxCmdAddFirst()
    {
        Transaction = db.TransactionManager.StartTransaction();
    }
    //命令结束提交事务
    public static void JoinBoxCmdAddLast()
    {
        if(Transaction.IsDisposed)
         return;
        if(Transaction.TransactionManager.NumberOfActiveTransactions != 0)
        {
            Transaction.Commit();
            Transaction.Dispose();
        }
    }
}

拒绝策略

namespace TestNameSpace
{
    public class AopTestClass
    {
        //类不拒绝,这里拒绝
        [JoinBoxRefuseInjectionTransaction]
        [CommandMethod("JoinBoxRefuseInjectionTransaction")]
        public void JoinBoxRefuseInjectionTransaction()
        {
        }

        //不拒绝
        [CommandMethod("InjectionTransaction")]
        public void InjectionTransaction()
        {
            //怎么用事务呢? 直接 AOP.Transaction
        }
    }   
    
    //拒绝注入事务,写类上,则方法全都拒绝
    [JoinBoxRefuseInjectionTransaction]
    public class AopTestClassRefuseInjection
    {
        //此时这个也是拒绝的..这里加特性是多余
        [JoinBoxRefuseInjectionTransaction]
        [CommandMethod("JoinBoxRefuseInjectionTransaction2")]
        public void JoinBoxRefuseInjectionTransaction2()
        {
            //拒绝注入就要自己开事务,通常用在循环提交事务上面.
            //另见 报错0x02 https://www.cnblogs.com/JJBox/p/10798940.html
            //就自己新建啊~
        }

        [CommandMethod("InjectionTransaction2")]
        public void InjectionTransaction2()
        {
        }
    }
}

(完)

posted @ 2022-04-17 22:00  惊惊  阅读(468)  评论(0编辑  收藏  举报