有使用 Castle DynamicProxy(Autofac.Extras.DynamicProxy 也是相依於它)實作 AOP 的朋友應該對 IInterceptor
這個介面不陌生,實作這個介面就能得到一個攔截方法的攔截器,但是目前 IInterceptor 只提供同步的版本,如果攔截的對象是非同步方法,事情就會變得麻煩一些,我們來看看該怎麼做?
先來說明一下使用情境,我們要做一個 LoggingInterceptor
,記錄目標方法的方法名稱
、方法參數
,以及回傳值
,目標方法如果是同步的,程式碼大概就長這樣:
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
// 取得方法名稱
var methodName = $"{invocation.MethodInvocationTarget.DeclaringType.FullName}.{invocation.Method.Name}()";
Log.WriteLine(methodName);
// 取得方法參數
var arguments = string.Join(
", ",
invocation.Method.GetParameters().Select((p, i) => $"{p.Name}={JsonSerializer.Serialize(invocation.Arguments[i])}"));
Log.WriteLine(arguments);
invocation.Proceed();
// 取得回傳值
var returnValue = $"r={JsonSerializer.Serialize(invocation.ReturnValue)}";
Log.WriteLine(returnValue);
}
}
如果我要欄截的目標方法是非同步的時候,上面這段程式碼執行起來是有問題的,假定我的目標方法長這樣:
public class MyService : IMyService
{
public async Task<int> Add(int a, int b)
{
var result = await Task.FromResult(a + b);
return result;
}
}
由於攔截器有一件要做的事情是記錄目標方法的回傳值,所以它必須在不清楚非同步方法實際回傳值型別的情況下,去取得回傳值。
Dynamic
國外有個大大比較幾個取得 Task Reuslt 的方法,發現用 dynamic object 是最快的,所以我們就直接使用 dynamic object 取得非同步方法的回傳值。
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
// 取得方法名稱
var methodName = $"{invocation.MethodInvocationTarget.DeclaringType.FullName}.{invocation.Method.Name}()";
Log.WriteLine(methodName);
// 取得方法參數
var arguments = string.Join(
", ",
invocation.Method.GetParameters().Select((p, i) => $"{p.Name}={JsonSerializer.Serialize(invocation.Arguments[i])}"));
Log.WriteLine(arguments);
invocation.Proceed();
if (invocation.Method.ReturnType.IsGenericType && invocation.Method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
{
((Task)invocation.ReturnValue).ContinueWith(
antecedent =>
{
dynamic d = antecedent;
// 取得回傳值
var returnValue = $"Returned={JsonSerializer.Serialize(d.Result)}";
Log.WriteLine(returnValue);
});
}
}
}
Castle.Core.AsyncInterceptor
除此之外,我們還可以使用 Castle.Core.AsyncInterceptor 這個套件,轉由它來處理攔截的工作,使用方法也很容易,建立一個類別實作 IAsyncInterceptor
。
public class LoggingAsyncInterceptor : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation)
{
throw new System.NotImplementedException();
}
public void InterceptAsynchronous(IInvocation invocation)
{
throw new System.NotImplementedException();
}
public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
throw new System.NotImplementedException();
}
}
有三個需要實作的方法:
- InterceptSynchronous:同步的攔截方法
- InterceptAsynchronous:非同步無回傳值的攔截方法
- InterceptAsynchronous<TResult>:非同步有回傳值的攔截方法
我這邊就只針對非同步有回傳值的攔截方法
進行實作,其他的就按照我們實際上的需求實作即可。
public class LoggingAsyncInterceptor : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation)
{
throw new System.NotImplementedException();
}
public void InterceptAsynchronous(IInvocation invocation)
{
throw new System.NotImplementedException();
}
public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
// 取得方法名稱
var methodName = $"{invocation.MethodInvocationTarget.DeclaringType.FullName}.{invocation.Method.Name}()";
Log.WriteLine(methodName);
// 取得方法參數
var arguments = string.Join(
", ",
invocation.Method.GetParameters().Select((p, i) => $"{p.Name}={JsonSerializer.Serialize(invocation.Arguments[i])}"));
Log.WriteLine(arguments);
invocation.Proceed();
((Task<TResult>)invocation.ReturnValue).ContinueWith(
antecedent =>
{
var result = antecedent.Result;
// 取得回傳值
var returnValue = $"Returned={JsonSerializer.Serialize(result)}";
Log.WriteLine(returnValue);
});
}
}
在原本的 Intercept 方法中轉由 IAsyncInterceptor 來處理攔截工作,就大功告成了。
public class LoggingInterceptor : IInterceptor
{
private readonly LoggingAsyncInterceptor asyncInterceptor;
public LoggingInterceptor()
{
this.asyncInterceptor = new LoggingAsyncInterceptor();
}
public void Intercept(IInvocation invocation)
{
this.asyncInterceptor.ToInterceptor().Intercept(invocation);
}
}
以上就提供有在用 Castle DynamicProxy 實作 AOP 的朋友參考,期待將來 Castle Project 能出一個自己的支援非同步方法的攔截器。