Castle Core

Castle Core,是 Castle Windsor 團隊開發的核心模組, 現在這個模組總共提供了三種功能,分別是 LoggingDictionaryAdapterDynamicProxy


Logging

由於 Castle Core 推出 Logging 功能時微軟尚未提供統一的日誌抽象函式庫 Microsoft.Extensions.Logging, 因為每套日誌系統都有自己的寫法與設定方式,所以 Logging 功能提供了 ILogger 抽象類型與抽象方法, 讓 log4netNLogSerilog 第三方日誌函式庫參考,能讓我們在切換日誌系統時更加方便。

不過這個功能基本上已經被微軟的 Microsoft.Extensions.Logging 官方函式庫取代了,所以 Castle Core 的 Logging 功能我們有基礎的了解就好, 目前主流的日誌函式庫都已提供對應的 Provider,能直接整合到 Microsoft.Extensions.Logging,因此實務上通常直接採用即可。


DictionaryAdapter

DictionaryAdapter 的用途是建立一層轉接機制,將原本弱型別的字典結構對應成強型別介面, 讓我們可以用物件屬性的方式來存取資料。這個功能適合用在動態結構、事前無法定義型別的資料。

下面這個範例透過 DictionaryAdapterFactory 將一個 HashtableISetting 介面建立關聯, 這樣我們就可以直接在執行時期建立強型別的物件,不需要自行做型別轉換。

void Main()
{
	var dictionary = new Hashtable();
	var factory = new DictionaryAdapterFactory();
	var adapter = factory.GetAdapter<ISetting>(dictionary);
	dictionary["Key"] = "123";
	Console.WriteLine(adapter.Key);
	adapter.Key = "456"; //直接透過屬性存取
	Console.WriteLine(adapter.Key);
}

public interface ISetting
{
	string Key { get; set;}
}

不過 DictionaryAdapter 背後是透過反射的機制來建立物件的,因此在效能與型別安全性上都有一定的成本。 若只是一般的設定管理,可以使用微軟官方的 Options Pattern 也可以將設定檔對應成強型別是比較好的選擇。


DynamicProxy

DynamicProxy 是 Castle Core 中最常被使用的功能,目的就是實現 AOP (Aspect-Oriented Programming,面向切面程式設計), 透過動態產生 Proxy 物件,我們能夠透過這個 Proxy 在程式碼的前後安插額外的程式碼,且不需要修改原本的業務程式碼。

核心概念是用 Proxy 將目標物件包裹在內部,並且在方法呼叫時進行攔截,所以像是效能量測、日誌紀錄、例外處理, 這種幾乎每個方法都需要的功能抽取出來,集中放在攔截器中統一管理,最大的優點就是讓商業邏輯與輔助程式碼分開獨立存放,讓程式碼保持乾淨整潔增加可讀性。

在 Castle Core 的 DynamicProxy 中我們需要透過實做 IInterceptor 介面來做到攔截的效果。

例如下面這段程式我想要了解執行 SayHello() 方法需要花費多少時間,一般來說就要在呼叫程式碼的地點前後都加上 Stopwatch 並且結束時 透過 Logger 的方式將結果紀錄到某個日誌系統。

void Main()
{
	var stopwatch = new Stopwatch();
	stopwatch.Start();
	SayHello();
	stopwatch.Stop();
	Console.WriteLine(stopwatch.Elapsed);
}

public void SayHello()
{
	Thread.Sleep(1000);
}

下面這段程式我們建立了 TimingInterceptor,其中的 invocation.Proceed() 用途就是執行原本的目標方法,也就是 SayHello(), 之後透過 ProxyGeneratorCreateInterfaceProxyWithTarget 方法將 IHello 介面與 TimingInterceptor 綁定成一個 Proxy, 這樣原本的 SayHello() 就可以保持乾淨,同時又能擴充額外的功能。

void Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var timingInterceptor = new TimingInterceptor();
	
	// 建立 Proxy 將 Hello 類別內的方法包裹在 timingInterceptor 之中
	var proxy = generator.CreateInterfaceProxyWithTarget(target, timingInterceptor); 
	
	// 透過 Proxy 執行方法
	proxy.SayHello();
}

public class Hello : IHello
{
	public void SayHello()
	{
		Thread.Sleep(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public void SayHello();
}

public class TimingInterceptor : IInterceptor
{
	public void Intercept(IInvocation invocation)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();
		invocation.Proceed(); // 開始執行 SayHello 方法
		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);
	}
}

攔截非同步方法與問題

由於非同步程式碼會立即返回,所以原本的計時攔截器會直接結束,所以想要攔截非同步方法需要額外的操作,下方的程式碼就是直接攔截非同步方法。

async Task Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var timingInterceptor = new TimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTarget(target, timingInterceptor);
	await proxy.SayHelloAsync();
}

public class Hello : IHello
{
	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public Task SayHelloAsync();
}

public class TimingInterceptor : IInterceptor
{
	public void Intercept(IInvocation invocation)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();
		invocation.Proceed();
		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);
	}
}

因此在攔截非同步方法時建議將回傳的 Task 傳入到 invocation.ReturnValue,

當執行到 invocation.Proceed() 時會將 SayHelloAsync() 方法回傳的 Task 設定到 invocation.ReturnValue 當中, 接下來透過 (Task)invocation.ReturnValue 將 Task 取出來並等待完成。

async Task Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var timingInterceptor = new TimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTarget(target, timingInterceptor);
	await proxy.SayHelloAsync();
}

public class Hello : IHello
{
	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public Task SayHelloAsync();
}

public class TimingInterceptor : IInterceptor
{
	public void Intercept(IInvocation invocation)
	{
		invocation.ReturnValue = InterceptAsync(invocation);
	}

	private async Task InterceptAsync(IInvocation invocation)
	{
		var stopwatch = Stopwatch.StartNew();
		invocation.Proceed(); // 將 SayHelloAsync Task 設定到 invocation.ReturnValue 中

		var task = (Task)invocation.ReturnValue; // 將 Task 取出
		await task.ConfigureAwait(false); // 等待 Task 完成

		stopwatch.Stop();
		Console.WriteLine($"Elapsed: {stopwatch.Elapsed}");
	}
}

第三方攔截非同步方法函式庫

從前面的範例可以看出,攔截非同步方法其實並不簡單,尤其是對於不熟悉 Task 與非同步流程的工程師,很難正確處理返回值與 await 的執行順序。 因此攔截非同步方法推薦使用第三方的 Castle.Core.AsyncInterceptor 函式庫,可以簡化開發難度並避免錯誤。

安裝後直接將實做的介面從原本的 IInterceptor 改成 IAsyncInterceptor,需要實做三個方法 InterceptSynchronous InternalInterceptAsynchronous InternalInterceptAsynchronous<TResult> 代表我們可以在同一個攔截器處理同步與非同步的方法,這種寫法與我們自行實做攔截器類似但將非同步與同步方法分開比較整潔。

async Task Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var asyncTimingInterceptor = new MyAsyncTimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTargetInterface<IHello>(target, asyncTimingInterceptor);
	await proxy.SayHelloAsync();
}

public class Hello : IHello
{
	public void SayHello()
	{
		Thread.Sleep(1000);
		Console.WriteLine("Hello");
	}

	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public void SayHello();
	public Task SayHelloAsync();
}

public class MyAsyncTimingInterceptor : IAsyncInterceptor
{
	public void InterceptAsynchronous(IInvocation invocation)
	{
		invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
	}

	public void InterceptAsynchronous<TResult>(IInvocation invocation)
	{
		invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
	}

	public void InterceptSynchronous(IInvocation invocation)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();
		invocation.Proceed();
		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);
	}

	private async Task InternalInterceptAsynchronous(IInvocation invocation)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();

		invocation.Proceed();
		var task = (Task)invocation.ReturnValue;
		await task;

		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);
	}

	private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();

		invocation.Proceed();
		var task = (Task<TResult>)invocation.ReturnValue;
		TResult result = await task;

		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);

		return result;
	}
}

第二種方式則是透過繼承 AsyncInterceptorBase 類別,內部其實也是實做 IAsyncInterceptor 介面, 可以省下處理 invocation.ReturnValue 的步驟。

另外 AsyncInterceptorBase 會在內部將同步方法包裝成 Task,所以不用寫同步攔截方法可以共用非同步攔截方法, 內部會將同步攔截方法轉換成非同步模式,可以避免傳統 ASP.NET 直接呼叫 Task.Wait() 或 Task.Result 所造成的 Deadlock。

但是同步方法在內部仍會透過 Task.Run(() => task).GetAwaiter().GetResult(); 等待結過,因此過程仍然是 Blocking, 只是以較安全的方式進行同步等待。

使用時要注意不要在呼叫 proceed 前 await 非同步操作,否則會出現 thread starvation 和 deadlocking。

async Task Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var asyncTimingInterceptor = new MyAsyncTimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTargetInterface<IHello>(target, asyncTimingInterceptor);
	await proxy.SayHelloAsync();
}

public class Hello : IHello
{
	public void SayHello()
	{
		Thread.Sleep(1000);
		Console.WriteLine("Hello");
	}

	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public void SayHello();
	public Task SayHelloAsync();
}

public class MyAsyncTimingInterceptor : AsyncInterceptorBase
{
	protected override async Task InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task> proceed)
	{
		var stopwatch = Stopwatch.StartNew();
		await proceed(invocation, proceedInfo).ConfigureAwait(false);
		stopwatch.Stop();
		Console.WriteLine($"Elapsed: {stopwatch.Elapsed}");
	}

	protected override async Task<TResult> InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed)
	{
		var stopwatch = Stopwatch.StartNew();
		var result = await proceed(invocation, proceedInfo).ConfigureAwait(false);
		stopwatch.Stop();
		Console.WriteLine($"Elapsed: {stopwatch.Elapsed}");
		return result;
	}
}

第三種方式繼承類別 ProcessingAsyncInterceptor<TState>,是這三種方式中最簡單並且最安全的方式。

可以看到繼承 ProcessingAsyncInterceptor<TState> 類別後就只需要處理 StartingInvocationCompletedInvocation, 分別代表呼叫前執行與呼叫後執行,並且把之前需要注意的細節隱藏起來讓不用我們自己處理,注意此類別不會將同步方法轉換成非同步。

async Task Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var asyncTimingInterceptor = new MyAsyncTimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTargetInterface<IHello>(target, asyncTimingInterceptor);
	await proxy.SayHelloAsync();
}

public class Hello : IHello
{
	public void SayHello()
	{
		Thread.Sleep(1000);
		Console.WriteLine("Hello");
	}

	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public void SayHello();
	public Task SayHelloAsync();
}

public class MyAsyncTimingInterceptor : ProcessingAsyncInterceptor<Stopwatch>
{
	protected override Stopwatch StartingInvocation(IInvocation invocation)
	{
		var stopwatch = Stopwatch.StartNew();
		stopwatch.Start();
		return stopwatch;
	}

	protected override void CompletedInvocation(IInvocation invocation, Stopwatch state)
	{
		state.Stop();
		Console.WriteLine(state.Elapsed);
	}
}

另外這種測試時間攔截器因為很常用,所以函式庫有內建時間的攔截器,這樣我們就不用在自己實做了。

下面就是直接繼承了函式庫提供的 AsyncTimingInterceptor,這樣我們只要處理呼叫前與呼叫後的日誌紀錄,就不用自己處理計時器了。

async Task Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var asyncTimingInterceptor = new MyAsyncTimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTargetInterface<IHello>(target, asyncTimingInterceptor);
	await proxy.SayHelloAsync();
}

public class Hello : IHello
{
	public void SayHello()
	{
		Thread.Sleep(1000);
		Console.WriteLine("Hello");
	}

	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public void SayHello();
	public Task SayHelloAsync();
}

public class MyAsyncTimingInterceptor : AsyncTimingInterceptor
{
	protected override void StartingTiming(IInvocation invocation)
	{
		Console.WriteLine($"{invocation.Method.Name}:StartingTiming");
	}

	protected override void CompletedTiming(IInvocation invocation, Stopwatch stopwatch)
	{
		Console.WriteLine($"{invocation.Method.Name}:CompletedTiming:{stopwatch.Elapsed:g}");
	}
}