肉夾饃是什么
肉夾饃(https://github.com/inversionhourglass/Rougamo)通過靜態代碼織入方式實現AOP的組件。.NET常用的AOP有Castle DynamicProxy、AspectCore等,以上兩種AOP組件都是通過運行時生成一個代理類執行AOP代碼的,肉夾饃則是在代碼編譯時直接修改原始方法IL代碼,在原始方法內織入AOP代碼的。.NET靜態AOP的組件或許有人使用過PostSharp,這是一個功能完善且強大的靜態代碼織入組件,Postsharp有社區版,但可惜的是社區版不支持異步方法,肉夾饃的實現方式與Postsharp類似,同時也支持了異步方法,如果你僅僅使用了Postsharp方法層級的AOP代碼織入功能,可以嘗試使用肉夾饃來替代Postsharp。
在?上一篇文章?中介紹了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(https://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="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"><Rougamo enumerable-returns="true" /></Weavers>
這個設定是因為狀態機并沒有保存所有的元素到一個集合中,每個元素都是一次一次調用MoveNext
執行代碼返回的,如果你使用foreach
遍歷Iterator
或AsyncIterator
,并且對每次遍歷的元素使用玩之后并沒有進行保存,那么上一個元素可能在你遍歷下一個元素時被GC回收。記錄它們的返回值的實現方式是額外建立一個集合保存每次迭代的元素值,這種方式對上面說的的foreach
遍歷的情況來說會產生額外的內存消耗,而如果迭代器的元素很多,或者每個元素本身很占內存,那么這種方式可能會額外占用大量內存空間,所以開啟這個開關前需要考慮一番。
最后
如果在使用肉夾饃的過程中遇到了什么問題,或者希望增加一些什么樣的功能,歡迎到github(https://github.com/inversionhourglass/Rougamo)里提issue
,不過對于新功能,可能會有一個較長的周期才能完成并發布正式版。
隨著SourceGenerator
的應用越來越廣泛,Mono.Cecil
的應用場景被進一步壓縮,一開始提到的動態代理現在也能通過SourceGenerator
在編譯時生成代理類,這是一件好事,相比晦澀易錯的IL,SourceGenerator
提供的語法樹更加方便易懂且不易出錯,但這并不代表Mono.Cecil
應該退場了(至少現在不是),Mono.Cecil
雖然門檻高,但他的功能也同樣強大,直接修改IL是SourceGenerator
和`Emit所無法做到的(至少現在是這樣),如果在以后的編程之路中遇到了
SourceGenerator和`Emit
無法解決的問題,希望你能想起還有Mono.Cecil
和Fody
這條路,如果有時間可以嘗試一下,也希望肉夾饃這個項目能給你帶來一些參考價值。