.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)
,但如果返回值為Task
或Task<T>
但並沒有使用async
寫法,那麼其值就是typeof(Task)
或typeof(Task<T>)
,這樣設定的好處是,你設置的返回值類型與該屬性的值相同即可,不用考慮方法是否非同步 - 不論是異常處理還是設置/修改返回值,設置的返回值類型必須與方法定義的返回類型(
MethodContext.RealReturnType
)相同,類型不同時運行時會報錯 OnExit
中調用MethodContext.ReplaceReturnValue
無法修改返回值
補充說明
在 上一篇文章 中由於是第一篇文章,介紹的東西較多,部分功能並沒有在文章中詳細說明,本篇由於篇幅較短,所以會補上一些說明,不過這裡也不會介紹全部的,詳細的介紹可以移步 github(//github.com/inversionhourglass/Rougamo)
Iterator / AsyncIterator 不支援修改返回值和異常處理
Iterator
和AsyncIterator
也就是下面的寫法
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 不支援記錄返回值
同樣的,Iterator
和AsyncIterator
默認也無法通過MethodContext.ReturnValue
獲取方法的返回值,但可以通過FodyWeavers.xml
的Rougamo
節點增加屬性配置enumerable-returns="true"
來記錄Iterator
和AsyncIterator
的返回值到MethodContext.ReturnValue
。
<Weavers xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Rougamo enumerable-returns="true" />
</Weavers>
這個設定是因為狀態機並沒有保存所有的元素到一個集合中,每個元素都是一次一次調用MoveNext
執行程式碼返回的,如果你使用foreach
遍歷Iterator
或AsyncIterator
,並且對每次遍歷的元素使用玩之後並沒有進行保存,那麼上一個元素可能在你遍歷下一個元素時被GC回收。記錄它們的返回值的實現方式是額外建立一個集合保存每次迭代的元素值,這種方式對上面說的的foreach
遍歷的情況來說會產生額外的記憶體消耗,而如果迭代器的元素很多,或者每個元素本身很占記憶體,那麼這種方式可能會額外佔用大量記憶體空間,所以開啟這個開關前需要考慮一番。
最後
如果在使用肉夾饃的過程中遇到了什麼問題,或者希望增加一些什麼樣的功能,歡迎到github(//github.com/inversionhourglass/Rougamo)里提issue
,不過對於新功能,可能會有一個較長的周期才能完成並發布正式版。
隨著SourceGenerator
的應用越來越廣泛,Mono.Cecil
的應用場景被進一步壓縮,一開始提到的動態代理現在也能通過SourceGenerator
在編譯時生成代理類,這是一件好事,相比晦澀易錯的IL,SourceGenerator
提供的語法樹更加方便易懂且不易出錯,但這並不代表Mono.Cecil
應該退場了(至少現在不是),Mono.Cecil
雖然門檻高,但他的功能也同樣強大,直接修改IL是SourceGenerator
和`Emit所無法做到的(至少現在是這樣),如果在以後的編程之路中遇到了
SourceGenerator和`Emit
無法解決的問題,希望你能想起還有Mono.Cecil
和Fody
這條路,如果有時間可以嘗試一下,也希望肉夾饃這個項目能給你帶來一些參考價值。