.NET中利用基於編譯時的AOP框架進行自動插入程式碼及優化重複程式碼
理想的程式碼優化方式
團隊日常協作中,自然而然的會出現很多重複程式碼,根據這些程式碼的種類,之前可能會以以下方式處理
方式 | 描述 | 應用時可能產生的問題 |
---|---|---|
硬編碼 | 多數新手,或逐漸腐壞的專案會這麼幹,會直接複製之前實現的程式碼 | 帶來的問題顯而易見的多,例如架構會逐漸隨時間被侵蝕,例外越來越多 |
提取函式 | 提取成為函式,然後複用 | 提取函式,然後複用,會比直接硬編碼好些,但是仍然存在大量因“例外”而導致增加引數、增加函式過載的情況 |
模板生成器 | CodeSmith/T4等 | 因為是獨立程序,所以對於讀取使用者程式碼或專案,實現難度較高,且需要現有使用者專案先生成成功,再進行生成 ,或者是完全基於新專案 |
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 是 postsharp 的後續作品。
下面作一個簡單示例
- 建立一個.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