[料理佳餚] Castle.DynamicProxy 非同步方法的攔截器

有使用 Castle DynamicProxyAutofac.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 能出一個自己的支援非同步方法的攔截器。

參考資料

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學