Rougamo、Fody 實現靜態Aop

Karl_Albright發表於2024-07-01

最近在看專案,看到別人使用Rougamo框架,好奇花了點時間仔細研究了,在這裡記錄一下。

0. 靜態編織 Aop

首先,我們先了解什麼是Aop? Aop 是指面向切面程式設計 (Aspect Oriented Programming),而所謂的切面,可以認為是具體攔截的某個業務點。

我們常用的aop框架是 AspectCore,他是屬於動態代理,也就是發生在執行時期間對程式碼進行“修改”。

Rougamo、Fody 是屬於靜態編織,是指在編譯階段將程式碼修改或額外的功能直接嵌入到程式集中,這個過程發生在原始碼被編譯成可執行檔案或庫之前。這意味著,一旦編譯完成,插入的程式碼就已經是程序集的一部分,無需在執行時再進行額外的操作。

1. Rougamo 肉夾饃

Rougamo 是一個開源專案,github: https://github.com/inversionhourglass/Rougamo,他是透過Fody -> Mono.Cecil 的方式實現靜態編織 實現Aop功能。

建立控制檯程式,Nuget安裝 Rougamo.Fody

[AttributeUsage(AttributeTargets.Method)]
public class LoggingAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        Console.WriteLine("執行方法 {0}() 開始,引數:{1}.", context.Method.Name, 
            JsonConvert.SerializeObject(context.Arguments));
    }
    public override void OnException(MethodContext context)
    {
        Console.WriteLine("執行方法 {0}() 異常,{1}.", context.Method.Name, context.Exception.Message);
    }
    public override void OnExit(MethodContext context)
    {
        Console.WriteLine("執行方法 {0}() 結束.", context.Method.Name);
    }
    public override void OnSuccess(MethodContext context)
    {
        Console.WriteLine("執行方法 {0}() 成功.", context.Method.Name);
    }
}
internal class Program
{
    static void Main(string[] args)
    {
        Add(1, 2);
        AddAsync(1, 2);
        Divide(1, 2);
    }

    [Logging]
    static int Add(int a, int b) => a + b;

    [Logging]
    static Task<int> AddAsync(int a, int b) => Task.FromResult(a + b);

    [Logging]
    static decimal Divide(decimal a, decimal b) => a / b;
}

執行後會自動建立FodyWeavers.xsd 和 FodyWeavers.xml

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
  <xs:element name="Weavers">
    <xs:complexType>
      <xs:all>
        <xs:element name="Rougamo" minOccurs="0" maxOccurs="1" type="xs:anyType" />
      </xs:all>
      <xs:attribute name="VerifyAssembly" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="VerifyIgnoreCodes" type="xs:string">
        <xs:annotation>
          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="GenerateXsd" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
    </xs:complexType>
  </xs:element>
</xs:schema>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Rougamo />
</Weavers>

下面是執行結果

這時候我們可以看到 增加了LoggingAttribute 特性的方法在執行前、執行成功、執行結束 執行了 OnEntry(MethodContext context) 、OnSuccess(MethodContext context)、OnExit(MethodContext context) 方法,這時我們開啟ILSpy工具,看看實際執行的程式碼

internal class Program
{
    private static void Main(string[] args)
    {
        Add(1, 2);
        AddAsync(1, 2);
        Divide(1m, 2m);
    }

    [DebuggerStepThrough]
    private static int Add(int a, int b)
    {
        LoggingAttribute loggingAttribute = new LoggingAttribute();
        IMo[] mos = new IMo[1] { loggingAttribute };
        MethodContext methodContext = new MethodContext(null, typeof(Program), MethodBase.GetMethodFromHandle((RuntimeMethodHandle)/*OpCode not supported: LdMemberToken*/, typeof(Program).TypeHandle), isAsync: false, isIterator: false, mosNonEntryFIFO: false, mos, new object[2] { a, b });
        loggingAttribute.OnEntry(methodContext);
        int result = default(int);
        if (methodContext.ReturnValueReplaced)
        {
            result = (int)methodContext.ReturnValue;
            loggingAttribute.OnExit(methodContext);
            return result;
        }
        if (methodContext.RewriteArguments)
        {
            a = (int)methodContext.Arguments[0];
            b = (int)methodContext.Arguments[1];
        }
        bool flag = default(bool);
        do
        {
            try
            {
                while (true)
                {
                    try
                    {
                        flag = false;
                        result = $Rougamo_Add(a, b);
                    }
                    catch (Exception exception)
                    {
                        methodContext.Exception = exception;
                        methodContext.Arguments[0] = a;
                        methodContext.Arguments[1] = b;
                        loggingAttribute.OnException(methodContext);
                        if (methodContext.RetryCount > 0)
                        {
                            continue;
                        }
                        if (methodContext.ExceptionHandled)
                        {
                            result = (int)methodContext.ReturnValue;
                            break;
                        }
                        throw;
                    }
                    break;
                }
            }
            finally
            {
                if (methodContext.HasException || methodContext.ExceptionHandled)
                {
                    goto IL_0160;
                }
                methodContext.ReturnValue = result;
                methodContext.Arguments[0] = a;
                methodContext.Arguments[1] = b;
                loggingAttribute.OnSuccess(methodContext);
                if (methodContext.RetryCount <= 0)
                {
                    if (methodContext.ReturnValueReplaced)
                    {
                        result = (int)methodContext.ReturnValue;
                    }
                    goto IL_0160;
                }
                flag = true;
                goto end_IL_00fc;
                IL_0160:
                loggingAttribute.OnExit(methodContext);
                end_IL_00fc:;
            }
        }
        while (flag);
        return result;
    }

    [DebuggerStepThrough]
    private static Task<int> AddAsync(int a, int b)
    {
        LoggingAttribute loggingAttribute = new LoggingAttribute();
        IMo[] mos = new IMo[1] { loggingAttribute };
        MethodContext methodContext = new MethodContext(null, typeof(Program), MethodBase.GetMethodFromHandle((RuntimeMethodHandle)/*OpCode not supported: LdMemberToken*/, typeof(Program).TypeHandle), isAsync: false, isIterator: false, mosNonEntryFIFO: false, mos, new object[2] { a, b });
        loggingAttribute.OnEntry(methodContext);
        Task<int> result = default(Task<int>);
        if (methodContext.ReturnValueReplaced)
        {
            result = (Task<int>)methodContext.ReturnValue;
            loggingAttribute.OnExit(methodContext);
            return result;
        }
        if (methodContext.RewriteArguments)
        {
            a = (int)methodContext.Arguments[0];
            b = (int)methodContext.Arguments[1];
        }
        bool flag = default(bool);
        do
        {
            try
            {
                while (true)
                {
                    try
                    {
                        flag = false;
                        result = $Rougamo_AddAsync(a, b);
                    }
                    catch (Exception exception)
                    {
                        methodContext.Exception = exception;
                        methodContext.Arguments[0] = a;
                        methodContext.Arguments[1] = b;
                        loggingAttribute.OnException(methodContext);
                        if (methodContext.RetryCount > 0)
                        {
                            continue;
                        }
                        if (methodContext.ExceptionHandled)
                        {
                            result = (Task<int>)methodContext.ReturnValue;
                            break;
                        }
                        throw;
                    }
                    break;
                }
            }
            finally
            {
                if (methodContext.HasException || methodContext.ExceptionHandled)
                {
                    goto IL_015b;
                }
                methodContext.ReturnValue = result;
                methodContext.Arguments[0] = a;
                methodContext.Arguments[1] = b;
                loggingAttribute.OnSuccess(methodContext);
                if (methodContext.RetryCount <= 0)
                {
                    if (methodContext.ReturnValueReplaced)
                    {
                        result = (Task<int>)methodContext.ReturnValue;
                    }
                    goto IL_015b;
                }
                flag = true;
                goto end_IL_00fc;
                IL_015b:
                loggingAttribute.OnExit(methodContext);
                end_IL_00fc:;
            }
        }
        while (flag);
        return result;
    }

    [DebuggerStepThrough]
    private static decimal Divide(decimal a, decimal b)
    {
        LoggingAttribute loggingAttribute = new LoggingAttribute();
        IMo[] mos = new IMo[1] { loggingAttribute };
        MethodContext methodContext = new MethodContext(null, typeof(Program), MethodBase.GetMethodFromHandle((RuntimeMethodHandle)/*OpCode not supported: LdMemberToken*/, typeof(Program).TypeHandle), isAsync: false, isIterator: false, mosNonEntryFIFO: false, mos, new object[2] { a, b });
        loggingAttribute.OnEntry(methodContext);
        decimal result = default(decimal);
        if (methodContext.ReturnValueReplaced)
        {
            result = (decimal)methodContext.ReturnValue;
            loggingAttribute.OnExit(methodContext);
            return result;
        }
        if (methodContext.RewriteArguments)
        {
            a = (decimal)methodContext.Arguments[0];
            b = (decimal)methodContext.Arguments[1];
        }
        bool flag = default(bool);
        do
        {
            try
            {
                while (true)
                {
                    try
                    {
                        flag = false;
                        result = $Rougamo_Divide(a, b);
                    }
                    catch (Exception exception)
                    {
                        methodContext.Exception = exception;
                        methodContext.Arguments[0] = a;
                        methodContext.Arguments[1] = b;
                        loggingAttribute.OnException(methodContext);
                        if (methodContext.RetryCount > 0)
                        {
                            continue;
                        }
                        if (methodContext.ExceptionHandled)
                        {
                            result = (decimal)methodContext.ReturnValue;
                            break;
                        }
                        throw;
                    }
                    break;
                }
            }
            finally
            {
                if (methodContext.HasException || methodContext.ExceptionHandled)
                {
                    goto IL_0160;
                }
                methodContext.ReturnValue = result;
                methodContext.Arguments[0] = a;
                methodContext.Arguments[1] = b;
                loggingAttribute.OnSuccess(methodContext);
                if (methodContext.RetryCount <= 0)
                {
                    if (methodContext.ReturnValueReplaced)
                    {
                        result = (decimal)methodContext.ReturnValue;
                    }
                    goto IL_0160;
                }
                flag = true;
                goto end_IL_00fc;
                IL_0160:
                loggingAttribute.OnExit(methodContext);
                end_IL_00fc:;
            }
        }
        while (flag);
        return result;
    }

    [Logging]
    private static int $Rougamo_Add(int a, int b)
    {
        return a + b;
    }

    [Logging]
    private static Task<int> $Rougamo_AddAsync(int a, int b)
    {
        return Task.FromResult(a + b);
    }

    [Logging]
    private static decimal $Rougamo_Divide(decimal a, decimal b)
    {
        return a / b;
    }
}

從實際執行的程式碼我們可以看到,原先Add(int a, int b)方法中的執行內容被移動到 $Rougamo_Add方法中,而Add(int a, int b)方法先是new LoggingAttribute() 和 new Rougamo.Context.MethodContext() -> 執行了 loggingAttribute.OnEntry(methodContext); -> 在do{}while(bool) 執行了$Rougamo_Add(a, b); -> 在 exception 中執行了loggingAttribute.OnException(methodContext); -> 在 finally中執行了 loggingAttribute.OnSuccess(methodContext); 和 loggingAttribute.OnExit(methodContext);

注:do{}while(bool) 執行了$Rougamo_Add(a, b); 是因為 Rougamo 可以實現方法執行失敗重試功能

至此我們明白了 Rougamo 實現 Aop功能是透過編譯時修改IL程式碼,往程式碼增加對應的生命週期程式碼。那他為什麼可以做到呢?其實是借用了Fody -> Mono.Cecil 的方式。

程式碼如下:https://gitee.com/Karl_Albright/csharp-demo/tree/master/FodyDemo/RougamoDemo

2. Fody -> Mono.Cecil

Fody 是一個開源專案,github: https://github.com/Fody/Fody,相關教程文件在 https://github.com/Fody/Home/tree/master/pages

建立類庫,選擇netstandard2.0,命名為HelloWorld,Nuget安裝 Fody 和 FodyPackaging

注:必須建立 netstandard2.0,因為FodyPackaging的目標是netstandard2.0,

在HelloWorld專案中,我們只放 HWAttribute類,繼承於 Attribute。程式碼如下

[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property)]
public class HWAttribute : Attribute
{

}

再次建立類庫,選擇netstandard2.0,命名為HelloWorld.Fody,Nuget安裝 FodyHelpers,引用HelloWorld類庫

在HelloWorld.Fody專案中,我們只放ModuleWeaver類(類名是固定的,詳情見Fody文件),繼承於 BaseModuleWeaver。程式碼如下

using Fody;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace HelloWorld.Fody
{
    public partial class ModuleWeaver : BaseModuleWeaver
    {
        public override void Execute()
        {
            foreach (var type in ModuleDefinition.Types)
            {
                foreach (var method in type.Methods)
                {
                    var customerAttribute = method.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == nameof(HWAttribute));
                    if (customerAttribute != null)
                    {
                        ProcessMethod(method);
                    }
                }
            }
        }

        public override IEnumerable<string> GetAssembliesForScanning()
        {
            yield return "mscorlib";
            yield return "System";
        }

        private MethodInfo _writeLineMethod => typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });

        private void ProcessMethod(MethodDefinition method)
        {
            // 獲取當前方法體中的第一個IL指令
            var processor = method.Body.GetILProcessor();
            var current = method.Body.Instructions.First();

            // 插入一個 Nop 指令,表示什麼都不做
            var first = Instruction.Create(OpCodes.Nop);
            processor.InsertBefore(current, first);
            current = first;

            // 構造 Console.WriteLine("Hello World")
            foreach (var instruction in GetInstructions(method))
            {
                processor.InsertAfter(current, instruction);
                current = instruction;
            }
        }
        private IEnumerable<Instruction> GetInstructions(MethodDefinition method)
        {
            yield return Instruction.Create(OpCodes.Nop);
            yield return Instruction.Create(OpCodes.Ldstr, "Hello World.");
            yield return Instruction.Create(OpCodes.Call, ModuleDefinition.ImportReference(_writeLineMethod));
        }
    }
}

在程式碼中,我們遍歷了所有型別的所有方法,如果方法標註了 HWAttribute特性,則增加 Console.WriteLine("Hello World."); 程式碼。

建立控制檯應用程式,命名為HelloWorldFodyDemo,新增 HelloWorld 和 HelloWorld.Fody 專案引用,並且手動增加 WeaverFiles標籤,目標是HelloWorld.Fody.dll

在控制檯中,我們需要一個方法,方法上有 HWAttribute 特性就可以了,程式碼如下

internal class Program
{
    static void Main(string[] args)
    {
        Echo();
        Console.ReadKey();
    }

    [HW]
    public static void Echo()
    {
        Console.WriteLine("Hello Fody.");
    }
}

在控制檯專案中,我們還需要 FodyWeavers.xml 和 FodyWeavers.xsd 檔案,(我也是從上面Rougamo專案中複製的),內容如下

<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
    <HelloWorld />
</Weavers>
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Weavers">
    <xs:complexType>
      <xs:all>
        <xs:element name="HelloWorld" minOccurs="0" maxOccurs="1" type="xs:anyType" />
      </xs:all>
      <xs:attribute name="VerifyAssembly" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="VerifyIgnoreCodes" type="xs:string">
        <xs:annotation>
          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
      <xs:attribute name="GenerateXsd" type="xs:boolean">
        <xs:annotation>
          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
        </xs:annotation>
      </xs:attribute>
    </xs:complexType>
  </xs:element>
</xs:schema>

目前,檔案結構如下

FodyDemo
|--- HelloWorld
     |--- HWAttribute.cs
     |--- HelloWorld.csproj
|--- HelloWorld.Fody
     |--- HelloWorld.Fody.csproj
     |--- ModuleWeaver.cs
|--- HelloWorldFodyDemo
     |--- FodyWeavers.xml
     |--- FodyWeavers.xsd
     |--- HelloWorldFodyDemo.csproj
     |--- Program.cs

程式碼如下:https://gitee.com/Karl_Albright/csharp-demo/tree/master/FodyDemo

最後執行結果如下,很明顯,HWAttribute生效了,我們成功的在Echo()方法前列印了Hello World。

我們再次開啟ILSpy工具,得到的結果如圖,程式碼增加了Console.WriteLine("Hello World.");行程式碼

4. Fody 有很多其他的“外掛”,大家可以多試試

AutoProperties.Fody: 這個外接程式為您提供了對自動屬性的擴充套件控制,比如直接訪問backing欄位或攔截getter和setter。

PropertyChanged.Fody: 將屬性通知新增到實現INotifyPropertyChanged的所有類。

InlineIL.Fody: 在編譯時注入任意IL程式碼。

MethodDecorator.Fody:透過IL重寫編譯時間裝飾器模式。

NullGuard.Fody: 將空引數檢查新增到程式集。

ToString.Fody: 給屬性生成ToString()方法

Rougamo.Fody: 在編譯時生效的AOP元件,類似於PostSharp。

相關文章