0%

Code 魔術師 - AOP using Fody

Hi all, 因為工作上的關係接觸到了所謂的 AOP 框架, 覺得挺有趣的故藉此文章分享。

Introduce

AOP(面向方面程式設計/Aspect-Oriented Programming), 他是一種寫程式的方法,簡單來說就是把一些常常要用到,但是跟主要業務邏輯無關的功能抽出來統一處理。
舉個例子, 假設我們在寫一個購物網站然後建立交易的流程如下:

public void 購物流程() {
寫入Log("開始購物");
檢查會員登入();
計算處理時間開始(); // for Tracing

// 真正的購物邏輯
檢查商品庫存();
建立訂單();
扣除庫存();

計算處理時間結束(); // for Tracing
寫入Log("購物完成");
}

看完之後會發現,實際上處理購物的邏輯只有中間那三行,但是我們前前後後要加一堆其他的東西 (Log, 登入, 計時等)。如果每個功能都要這樣寫,程式碼就會變得很亂,而且重複的程式碼到處都是。
這時候就可以用AOP的想法來處理,我們把跟購物無關的東西抽出來,流程如下:

@需要登入
@要記錄Log
@計算執行時間
public void 購物流程() {
// 只要寫真正要做的事就好
檢查商品庫存();
建立訂單();
扣除庫存();
}

看出來了嗎? 這個是不是很像後端API的ActionFilter 或是 Middleware的概念,但其實概念差不多只是再應用場景有些許的不同

比較特性 執行時機 作用範圍 使用場景
Middleware HTTP Request Pipeline 最早期 整個 Application 身分驗證, 請求加解密, CORS 處理, 異常處理
Action Filter Controller/Action 執行前後 限於 Controllers Model 驗證, Action 權限檢查, 回應格式處理
AOP 任何方法執行時期 任何類別或方法 方法層級快取, 效能監控, 交易管理, 記錄日誌

個人使用場景

我自己的使用場景,是要為了做一個替專案進行可觀側性監控的SDK,但不希望再改到使用端的code 的限制下進行設計。
那此次的SDK 撰寫是使用 Fody 作為AOP框架提供,以下是基本的應用範例。

Demo Project

Startup Main Project | Part I

  • 目的:在呼叫專案內任何method 前必須執行一特定method

以下是我們的 API main flow code

[Route("api/[controller]")]
public class DemoController(IDemoService demoService) : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
demoService.Echo();
return Ok("Hello World");
}
}

public class DemoService(IDemoRepository demoRepository) : IDemoService
{
public void Echo()
{
demoRepository.Echo();
}
}

public class DemoRepository : IDemoRepository
{
public void Echo()
{
throw new NotImplementedException();
}
}

Startup AOP

接著我們就可以安裝以下 nuget 套件:

  • Fody \rightarrow 專案本體
  • FodyHelper \rightarrow Library 專案

首先,我們必須建立一個專案,名字暫定叫做 AOP-Library.Fody,這邊要記住由於 Fody 的特性,這裡的專案名必須要有.Fody 作為後綴詞(理論上有另一種不需要多添加後綴詞的解法,礙於篇幅就先不敘述),接著來撰寫要讓AOP做的事情,code如下:

MethodLogger

此為,攔截 method

public static class MethodLogger
{
public static void Log(string methodName)
{
Console.WriteLine($"Interceptor get: {methodName}");
}
}

ModuleWeaver

在這邊進行 IL code 注入,達到攔截效果

public class ModuleWeaver : BaseModuleWeaver
{
public override void Execute()
{
var logMethod = typeof(MethodLogger).GetMethod(nameof(MethodLogger.Log));
var logMethodRef = ModuleDefinition.ImportReference(logMethod);

foreach (var type in ModuleDefinition.Types)
{
if (ShouldProcessType(type))
{
foreach (var method in type.Methods)
{
if (ShouldProcessMethod(method))
{
var il = method.Body.GetILProcessor();
method.Body.InitLocals = true;

var first = method.Body.Instructions.First();

var loadMethodName = il.Create(OpCodes.Ldstr, method.FullName);

var callLog = il.Create(OpCodes.Call, logMethodRef);

il.InsertBefore(first, loadMethodName);
il.InsertAfter(loadMethodName, callLog);

method.Body.Optimize();
}
}
}

}
}

public override IEnumerable<string> GetAssembliesForScanning()
{
yield return "netstandard";
yield return "mscorlib";
yield return "System.Runtime";
yield return "System.Console";
}

private bool ShouldProcessType(TypeDefinition type)
{
// 跳過系統類型、介面和特殊類型
return !type.IsInterface &&
!type.IsEnum &&
!type.IsAbstract &&
!type.IsValueType &&
!type.FullName.StartsWith("System.") &&
!type.FullName.StartsWith("<");

}

private bool ShouldProcessMethod(MethodDefinition method)
{
return method.HasBody &&
!method.IsConstructor &&
!method.IsGetter &&
!method.IsSetter &&
!method.IsAbstract &&
!method.IsRuntime &&
!method.IsPInvokeImpl;
}
}

Startup Main Project | Part II

設定好上述的AOP注入後,我們還需要來 Main Project這邊針對 csproj 做些事情,如下:

Csproj

<Project>
...
<ItemGroup>
<ProjectReference Include="..\AOP-Library.Fody\AOP-Library.Fody.csproj" />
<WeaverFiles Include="$(OutputPath)AOP-Library.Fody.dll"/>
</ItemGroup>
</Project>

Run Project

在這邊就可以嘗試把專案跑起來看看,如果一切順利的話,Console畫面會跟下圖一樣。

img.png

Conclusion

以上就是使用 Fody 作為AOP框架的應用實作,但其實AOP 還有另一個框架工具:Castle.Core,那兩者雖然都是AOP框架,但使用上的方式截然不同,若有空的話我再來寫一篇 Castle.Core的應用實作,今天先到這。