.NET靜態代碼織入——肉夾饃(Rougamo) 發佈1.1.0

肉夾饃(//github.com/inversionhourglass/Rougamo)通過靜態代碼織入方式實現AOP的組件,其主要特點是在編譯時完成AOP代碼織入,相比動態代理可以減少應用啟動的初始化時間讓服務更快可用,同時還能對靜態方法進行AOP。
上一篇文章 中介紹了1.0.0版本肉夾饃的功能,1.0.0版本能夠進行的AOP操作主要是日誌記錄以及APM操作,給出的示例項目也是OpenTelemetry的APM項目。在上一篇文章的評論以及github issue中都有朋友詢問是否能處理異常以及修改返回值等操作,最終拖了較長一段時間於近期發佈了1.1.0版本實現了這些功能。

快速開始

# 添加NuGet引用
dotnet add package Rougamo.Fody
public class TestService
{
    [Fact]
    public async void Test1()
    {
        var v1 = await M1();
        Assert.Null(v1);

        var v2 = Sum(1, null);
        Assert.Equal(-1, v2);

        var v3 = await M2();
        Assert.Empty(v3);
    }

    [MuteException]
    public async Task<string> M1()
    {
        throw new NotImplementedException();
    }

    [ArgNullCheck]
    public int Sum(int? a, int? b)
    {
        return a.Value + b.Value;
    }

    [ReturnNullCheck]
    public async Task<string> M2()
    {
        await Task.Yield();
        return null;
    }
}

public class MuteExceptionAttribute : MoAttribute
{
    public override void OnException(MethodContext context)
    {
        if (context.RealReturnType == typeof(string))
        {
            context.HandledException(this, null);
        }
    }
}

public class ArgNullCheckAttribute : MoAttribute
{
    public override void OnEntry(MethodContext context)
    {
        foreach (var arg in context.Arguments)
        {
            if (arg == null)
            {
                context.ReplaceReturnValue(this, -1);
            }
        }
    }
}

public class ReturnNullCheckAttribute : MoAttribute
{
    public override void OnSuccess(MethodContext context)
    {
        if (context.ReturnValue == null)
        {
            context.ReplaceReturnValue(this, string.Empty);
        }
    }
}

在上面的示例代碼中MuteExceptionAttribute重寫了OnException通過MethodContext.HandledException表明異常已處理並將返回值設置為null
ArgNullCheckAttribute重寫了OnEntry通過MethodContext.ReplaceReturnValue設置了返回值,由於OnEntry是在執行方法前調用,這種方式會在OnEntry執行完畢之後直接將ReplaceReturnValue設置的返回值作為方法的返回值直接返回,一般參數驗證、緩存邏輯會用到;
ReturnNullCheckAttribute重寫了OnSuccess通過MethodContext.ReplaceReturnValue修改了實際的返回值,示例中通過這種方式避免返回null值。

注意事項

  • 如果方法是async Task那麼MethodContext.RealReturnType取值為typeof(void),如果是async Task<T>那麼取值為typeof(T),但如果返回值為TaskTask<T>但並沒有使用async寫法,那麼其值就是typeof(Task)typeof(Task<T>),這樣設定的好處是,你設置的返回值類型與該屬性的值相同即可,不用考慮方法是否異步
  • 不論是異常處理還是設置/修改返回值,設置的返回值類型必須與方法定義的返回類型(MethodContext.RealReturnType)相同,類型不同時運行時會報錯
  • OnExit中調用MethodContext.ReplaceReturnValue無法修改返回值

補充說明

上一篇文章 中由於是第一篇文章,介紹的東西較多,部分功能並沒有在文章中詳細說明,本篇由於篇幅較短,所以會補上一些說明,不過這裡也不會介紹全部的,詳細的介紹可以移步 github(//github.com/inversionhourglass/Rougamo)

Iterator / AsyncIterator 不支持修改返回值和異常處理

IteratorAsyncIterator也就是下面的寫法

public IEnumerable<int> Iterator(int count)
{
    yield return 1;
    yield return 2;
    yield return 3;
}

public async IAsyncEnumerable<int> AsyncIterator(int count)
{
    yield return 3;
    await Task.Yield();
    yield return 2;
    await Task.Yield();
    yield return 1;
}

之所以不支持,是因為它們並不直接返回一個集合,而是返回一個狀態機(StateMachine),使用foreach迭代時實際每次迭代執行狀態機的MoveNext方法獲取本次迭代的返回值,考慮到實現這種特殊機制的複雜性以及平時使用的頻率,當前對此種類型不進行支持。

Iterator / AsyncIterator 不支持記錄返回值

同樣的,IteratorAsyncIterator默認也無法通過MethodContext.ReturnValue獲取方法的返回值,但可以通過FodyWeavers.xmlRougamo節點增加屬性配置enumerable-returns="true"來記錄IteratorAsyncIterator的返回值到MethodContext.ReturnValue

<Weavers xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Rougamo enumerable-returns="true" />
</Weavers>

這個設定是因為狀態機並沒有保存所有的元素到一個集合中,每個元素都是一次一次調用MoveNext執行代碼返回的,如果你使用foreach遍歷IteratorAsyncIterator,並且對每次遍歷的元素使用玩之後並沒有進行保存,那麼上一個元素可能在你遍歷下一個元素時被GC回收。記錄它們的返回值的實現方式是額外建立一個集合保存每次迭代的元素值,這種方式對上面說的的foreach遍歷的情況來說會產生額外的內存消耗,而如果迭代器的元素很多,或者每個元素本身很占內存,那麼這種方式可能會額外佔用大量內存空間,所以開啟這個開關前需要考慮一番。

最後

如果在使用肉夾饃的過程中遇到了什麼問題,或者希望增加一些什麼樣的功能,歡迎到github(//github.com/inversionhourglass/Rougamo)里提issue,不過對於新功能,可能會有一個較長的周期才能完成並發佈正式版。
隨着SourceGenerator的應用越來越廣泛,Mono.Cecil的應用場景被進一步壓縮,一開始提到的動態代理現在也能通過SourceGenerator在編譯時生成代理類,這是一件好事,相比晦澀易錯的IL,SourceGenerator提供的語法樹更加方便易懂且不易出錯,但這並不代表Mono.Cecil應該退場了(至少現在不是),Mono.Cecil雖然門檻高,但他的功能也同樣強大,直接修改IL是SourceGenerator和`Emit所無法做到的(至少現在是這樣),如果在以後的編程之路中遇到了SourceGenerator和`Emit無法解決的問題,希望你能想起還有Mono.CecilFody這條路,如果有時間可以嘗試一下,也希望肉夾饃這個項目能給你帶來一些參考價值。