Castle Core With Autofac

在上一篇 Castle Core 基礎教學 中,我們介紹了如何使用 DynamicProxy 實作 AOP 功能。

不過,手動建立 ProxyGenerator 並使用 CreateInterfaceProxyWithTarget 進行介面綁定, 實務上相對繁瑣。所以通常會搭配 IOC 容器,讓 AOP 整合更順暢。

目前常見的方案:

  1. Castle Windsor 使用 Castle 團隊開發的 IOC 容器叫做 Castle Windsor, 但它也是比較古老的專案,在微軟官方的 IOC 容器推出前就已經存在。

  2. Castle Core With Autofac 使用 Autofac 搭配官方擴充庫 Autofac.Extras.DynamicProxy, 底層使用 Castle Core 來實現 DynamicProxy 功能。 建議配合 Autofac.Extensions.DependencyInjection 使用,與 Microsoft DI 抽象介面兼容。

安裝函式庫與實做

所以接下來只要安裝這兩個函式庫即可整合 Autofac IOC 容器與 Castle Core 的 DynamicProxy 功能。

dotnet add package Autofac.Extensions.DependencyInjection
dotnet add package Autofac.Extras.DynamicProxy

使用 EnableInterfaceInterceptors() 註冊介面攔截器,並可透過兩種方式綁定攔截器: 一種是透過 InterceptedBy 方法直接在服務註冊時,指定特定介面與攔截器進行綁定。

void Main()
{
	var builder = new ContainerBuilder();

	builder.RegisterType<Hello>()
		.As<IHello>()
		.InterceptedBy(typeof(TimingInterceptor))
		.EnableInterfaceInterceptors();

	builder.Register(c => new TimingInterceptor());
	var container = builder.Build();
	var service = container.Resolve<IHello>();
	service.SayHello();
}

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

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

public interface IHello
{
	public void SayHello();
}

另一種使用 InterceptAttribute 直接在需要的方法加上屬性與要使用的攔截器即可。

void Main()
{
	var builder = new ContainerBuilder();
	
	builder.RegisterType<Hello>()
		.As<IHello>()
		.EnableInterfaceInterceptors();
		
	builder.Register(c => new TimingInterceptor());
	var container = builder.Build();
	var service = container.Resolve<IHello>();
	service.SayHello();
}

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

[Intercept(typeof(TimingInterceptor))]
public class Hello : IHello
{
	public void SayHello()
	{
		Thread.Sleep(1000);
		Console.WriteLine("Hello");
	}
}

public interface IHello
{
	public void SayHello();
}

透過 Autofac.Extras.DynamicProxy,我們不需要每次手動建立 ProxyGenerator,也不用擔心記憶體消耗問題, 只需專注於商業邏輯與攔截器實作,容器會自動處理代理生成與介面綁定。


接下來也加入 Castle.Core.AsyncInterceptor 函式庫用來攔截非同步方法。

dotnet add package Autofac.Extensions.DependencyInjection
dotnet add package Autofac.Extras.DynamicProxy
dotnet add package Castle.Core.AsyncInterceptor

由於 Autofac.Extras.DynamicProxy 只看得懂 IInterceptor 介面,上一篇文章寫的非同步攔截器都是實做 IAsyncInterceptor 介面, 所以直接註冊會導致在執行階段發生型別轉換錯誤,因此必須額外加入一層轉接將基於 IAsyncInterceptor 介面的攔截器轉換成 IInterceptor 介面的攔截器。

Castle.Core.AsyncInterceptor 提供了 AsyncDeterminationInterceptor 類別,它本身是一個轉接器且能自動判斷該使用同步或非同步的攔截方法, 透過它,我們可以將一個實作 IAsyncInterceptor 的攔截器包裝成 IInterceptor,從而與 Autofac.Extras.DynamicProxy 整合在一起。

透過 RegisterAssemblyTypes 將目前 Assembly 的類別與 MyTimingInterceptor 綁定在一起,這樣就能做到全局 AOP 的效果。

async Task Main()
{
	var builder = new ContainerBuilder();

	builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
				.AsImplementedInterfaces()
				.InstancePerLifetimeScope()
				.EnableInterfaceInterceptors()
				//.InterceptedBy(typeof(MyAsyncTimingInterceptor)); // 直接綁定非同步攔截器會產生轉型錯誤
				.InterceptedBy(typeof(MyTimingInterceptor));

	builder.RegisterType<MyTimingInterceptor>();
	builder.RegisterType<MyAsyncTimingInterceptor>();

	var container = builder.Build();
	var service = container.Resolve<IHello>();
	await service.SayHelloAsync();
}

// 轉換成基於 `IInterceptor` 介面的攔截器,內部會自動將流程轉到 MyAsyncTimingInterceptor 處理。
public class MyTimingInterceptor : AsyncDeterminationInterceptor
{
	public MyTimingInterceptor(MyAsyncTimingInterceptor interceptor)
		: base(interceptor)
	{
	}
}

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

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

public interface IHello
{
	public Task SayHelloAsync();
}