介紹如何通過使用基於Roslyn的編譯時AOP框架來解決.NET項目的代碼復用問題。 可以在項目編譯時自動插入指定代碼,從而避免在運行時帶來的性能消耗。 ...
理想的代碼優化方式
團隊日常協作中,自然而然的會出現很多重覆代碼,根據這些代碼的種類,之前可能會以以下方式處理
方式 | 描述 | 應用時可能產生的問題 |
---|---|---|
硬編碼 | 多數新手,或逐漸腐壞的項目會這麼乾,會直接複製之前實現的代碼 | 帶來的問題顯而易見的多,例如架構會逐漸隨時間被侵蝕,例外越來越多 |
提取函數 | 提取成為函數,然後復用 | 提取函數,然後復用,會比直接硬編碼好些,但是仍然存在大量因“例外”而導致增加參數、增加函數重載的情況 |
模板生成器 | CodeSmith/T4等 | 因為是獨立進程,所以對於讀取用戶代碼或項目,實現難度較高,且需要現有用戶項目先生成成功,再進行生成 ,或者是完全基於新項目 |
代碼片段 | VS自帶的代碼片段功能 | 無法對複雜的環境或條件做出響應 |
AOP框架 | 面向切麵編程,可以解決很多於用戶代碼前後增加操作的事情 | 但是大多AOP框架都是基於透明代理形式實現的,對於相互調用較多的代碼,但形成性能壓力,而且因為要符合透明代理的規則,所以要提供相應的子類或介面。 |
基於Rosyln的編譯時插入代碼
但以上這幾種,AOP算是最理想的方式,但是感覺上還可以有更好的解決方案。
直到讀到了這篇文章 Introducing C# Source Generators,文中提供了一種新的解決方案,即通過Roslyn
的Source Generator
在編譯時直接讀取當前項目中的語法樹,處理並生成的新代碼,然後在編譯時也使用這些新代碼。
那麼如果可以讀取現有代碼的語法樹,通過讀取代碼中的標記,那麼在代碼生成過程中是否就能直接生成。
實現如下效果:
項目中的源代碼 Program.cs
internal class Program
{
[Log]
private static int Add( int a, int b )
{
return a + b;
}
}
自動根據 LogAttribute
自動編譯成的代碼 Program.g.cs
internal class Program
{
[Log]
private static int Add( int a, int b )
{
Console.WriteLine("Program.Add(int, int) 開始運行.");
int result;
result = a + b;
Console.WriteLine("Program.Add(int, int) 結束運行.");
return result;
}
}
當然LogAttribute
中需要去實現插入代碼。
然後項目自動使用新生成的Program.g.cs
進行編譯。這樣就實現了基於編譯時的AOP。
即實現以下流程
使用Metalama實現以上流程
經過尋找,發現其實已經有框架可以實現我上面說的流程了,也就是在編譯時實現代碼的插入。
https://www.postsharp.net/metalama 。
下麵作一個簡單示例
- 創建一個.NET6.0的控制台應用,我這裡命名為
LogDemo
,
其中的入口文件Program.cs
namespace LogDemo {
public class Program
{
public static void Main(string[] args)
{
var r = Add(1, 2);
Console.WriteLine(r);
}
// 這裡寫一個簡單的方法,一會對這個方法進行代碼的插入
private static int Add(int a, int b)
{
var result = a + b;
Console.WriteLine("Add" + result);
return result;
}
}
}
- 在項目中使用Metalama
通過引用包 https://www.nuget.org/packages/Metalama.Framework, 註意Metalama當前是Preview版本,如果通過可視化Nuget管理器引入,需要註意勾選包含預發行版
dotnet add package Metalama.Framework --version 0.5.7-preview
- 編寫一個AOP的Attribute
在項目中引入 Metalama.Framework
後無需多餘配置或代碼,直接編寫一個AOP的Attribute
using Metalama.Framework.Aspects;
namespace LogDemo {
public class Program
{
public static void Main(string[] args)
{
var r = Add(1, 2);
Console.WriteLine(r);
}
// 在這個方法中使用了下麵的Attribute
[LogAttribute]
private static int Add(int a, int b)
{
var result = a + b;
Console.WriteLine("Add" + result);
return result;
}
}
// 這裡是增加的 Attribute
public class LogAttribute : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
Console.WriteLine(meta.Target.Method.ToDisplayString() + " 開始運行.");
var result = meta.Proceed();
Console.WriteLine(meta.Target.Method.ToDisplayString() + " 結束運行.");
return result;
}
}
}
- 執行結果如下
Program.Add(int, int) 開始運行.
Add3
Program.Add(int, int) 結束運行.
3
- 生成的程式集進行反編譯,得到的代碼如下:
using Metalama.Framework.Aspects;
namespace LogDemo {
public class Program
{
public static void Main(string[] args)
{
var r = Add(1, 2);
Console.WriteLine(r);
}
// 在這個方法中使用了下麵的Attribute
[LogAttribute]
private static int Add(int a, int b)
{
Console.WriteLine("Program.Add(int, int) 開始運行.");
int result_1;
var result = a + b;
Console.WriteLine("Add" + result);
result_1 = result;
Console.WriteLine("Program.Add(int, int) 結束運行.");
return result_1;
}
}
#pragma warning disable CS0067
// 這裡是增加的 Attribute
public class LogAttribute : OverrideMethodAspect
{
public override dynamic? OverrideMethod() =>
throw new System.NotSupportedException("Compile-time-only code cannot be called at run-time.");
}
#pragma warning restore CS0067
}
總結
這樣就完全實現了我之前想要的效果,當然使用Metalama
還可以實現很多能極大地提高生產力的功能,它不僅可以對方法進行改寫,也可以對屬性、欄位、事件、甚至是類、命名空間進行一些操作 。
引用
Introducing C# Source Generators:https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/
Metalama官網:https://www.postsharp.net/metalama