Castle Core

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




Logging

由於 Castle Core 的 Logging 功能推出的當下微軟還沒有提供類似的日誌功能,所以在早期寫 .net framework 的時候還要安裝額外 例如 log4netNLogSerilog 這種第三方日誌函式庫,不過由於這些函式庫當初並沒有使用共同的抽象方法,導致每套日誌系統都有自己的寫法與設定方式, 所以當時要切換日誌系統是非常困難的,在這種背景下 Castle Core 的 Logging 功能提供了 ILogger 抽象類型與抽象方法, 能讓我們在切換日誌系統時更加方便。

不過這個功能基本上已經被微軟的 Microsoft.Extensions.Logging 官方函式庫取代了,所以 Castle Core 的 Logging 功能我們有基礎的了解就好, 現在實務上各家的日誌系統都有提供對應 Microsoft.Extensions.Logging 的 Provider 我們直接使用就好了。




DictionaryAdapter

這個功能可以建立一個轉接層將弱型別的字典變成強型別,可以用在例如設定檔這種弱型別的資料。

下面這個範例建立了一個 DictionaryAdapterFactory 並且將 dictionaryISetting 介面建立一個中間層, 這樣我們就可以直接在執行時期建立強型別的物件。

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

這個功能就比較重要了,主要就是為了實現 AOP 才會安裝 Castle Core 這個函式庫,另外有提供 DynamicProxy 功能的函式庫還有 AspectCore

DynamicProxy 主要的功能就是建立一個動態的 Proxy,我們能夠透過這個 Proxy 在程式碼的前後安插額外的程式碼,例如要測試一個方法的執行時間, 我們就可以透過 DynamicProxy 在不修改想要測試的方法的情況下添加測試的程式碼。

這種類型的功能都是透過攔截的機制在程式碼的執行前後安插額外的程式碼,在 Castle Core 中我們需要透過實做 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();
	var proxy = generator.CreateInterfaceProxyWithTarget(target, timingInterceptor);
	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();
		stopwatch.Stop();
		Console.WriteLine(stopwatch.Elapsed);
	}
}

但是 Castle Core 對於攔截非同步方法的寫法與規則並不是很容易了解,所以推薦是使用第三方的 Castle.Core.AsyncInterceptor 函式庫。

安裝後直接將實做的介面從原本的 IInterceptor 改成 IAsyncInterceptor,需要實做三個方法 InterceptSynchronousInternalInterceptAsynchronousInternalInterceptAsynchronous<TResult>,代表我們可以在同一個攔截器處理同步與非同步的方法。

void Main()
{
	IHello target = new Hello();
	ProxyGenerator generator = new ProxyGenerator();
	var asyncTimingInterceptor = new MyAsyncTimingInterceptor();
	var proxy = generator.CreateInterfaceProxyWithTargetInterface<IHello>(target, asyncTimingInterceptor);
	proxy.SayHello();
}

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

	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
	}
}

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;
	}
}

上面這種寫法會產生許多相同的程式碼可以選擇重構,或者是繼承另外一個類別 ProcessingAsyncInterceptor<TState>

可以看到繼承 ProcessingAsyncInterceptor<TState> 類別後就只需要處理 StartingInvocationCompletedInvocation, 分別是呼叫前執行與呼叫後執行,就不用分成同步與非同步多種版本了。

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


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

	public async Task SayHelloAsync()
	{
		await Task.Delay(1000);
	}
}

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

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

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

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

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

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}");
	}
}