web-dev-qa-db-ja.com

DynamicProxyを使用して非同期メソッドの呼び出しをインターセプトします

以下は、 Castle Dynamic Proxy ライブラリのInterceptを実装するカスタムタイプのIInterceptorメソッドのコードです。このスニペットは、投稿された [〜#〜] aop [〜#〜] ベースのロギング概念実証コンソールアプリからのものです ここ

    public void Intercept(IInvocation invocation)
    {
        if (Log.IsDebugEnabled) Log.Debug(CreateInvocationLogString("Called", invocation));
        try
        {
            invocation.Proceed();
            if (Log.IsDebugEnabled)
                if (invocation.Method.ReturnType != typeof(void))
                    Log.Debug("Returning with: " + invocation.ReturnValue);
        }
        catch (Exception ex)
        {
            if (Log.IsErrorEnabled) Log.Error(CreateInvocationLogString("ERROR", invocation), ex);
            throw;
        }
    }

これは通常のメソッド呼び出しでは期待どおりに機能しますが、asyncメソッドで試行した場合は機能しません(C#5.0のasync/awaitキーワードを使用)。そして、私はこれの背後にある理由も理解していると信じています。

async/awaitが機能するために、コンパイラーはメソッドの機能本体をバックグラウンドでステートマシンに追加し、最初のawaitable式が機能しなくなるとすぐに、コントロールは呼び出し元に戻ります。同期的に完了しました。

また、戻り値の型を調べて、次のようなasyncメソッドを扱っているかどうかを判断することもできます。

            if (invocation.Method.ReturnType == typeof(Task) || 
                (invocation.Method.ReturnType.IsGenericType && 
                 invocation.Method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
                Log.Info("Asynchronous method found...");

これは、asyncまたはTask<>のいずれかを返すTaskメソッドでのみ機能し、voidでは機能しませんが問題ありません。

Interceptが元の呼び出し元ではなくそこに戻るように、awaiterメソッド内でどのような変更を加える必要がありますか?

27

おそらく「問題」は、タスクを返していることをログに記録しているだけであり、そのタスク内にvalueが必要ですか?

その場合でも、タスクが完了するのを待たずに、すぐに呼び出し元にタスクを返す必要があります。あなたがそれを壊すならば、あなたは根本的に物事を台無しにしている。

ただし、タスクを呼び出し元に返す前に、継続を追加する必要があります( Task.ContinueWith )タスクが完了したときに結果(または失敗)をログに記録します。それでも結果情報は得られますが、もちろん、他のログの後にログに記録される可能性があります。 また戻る直前にログを記録して、次のようなログを作成することもできます。

Called FooAsync
Returned from FooAsync with a task
Task from FooAsync completed, with return value 5

タスクから結果を取得するビジネス(正常に完了した場合)は、リフレクションを使用して実行する必要がありますが、これは少し面倒です。または、動的型付けを使用することもできます。 (どちらの方法でも、パフォーマンスが少し低下します。)

19
Jon Skeet

ジョンの答えのおかげで、これは私が最終的に得たものです:

public void Intercept(IInvocation invocation)
{
    if (Log.IsDebugEnabled) Log.Debug(CreateInvocationLogString("Called", invocation));
    try
    {
        invocation.Proceed();

        if (Log.IsDebugEnabled)
        {
            var returnType = invocation.Method.ReturnType;
            if (returnType != typeof(void))
            {
                var returnValue = invocation.ReturnValue;
                if (returnType == typeof(Task))
                {
                    Log.Debug("Returning with a task.");
                }
                else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
                {
                    Log.Debug("Returning with a generic task.");
                    var task = (Task)returnValue;
                    task.ContinueWith((antecedent) =>
                                          {
                                              var taskDescriptor = CreateInvocationLogString("Task from", invocation);
                                              var result =
                                                  antecedent.GetType()
                                                            .GetProperty("Result")
                                                            .GetValue(antecedent, null);
                                              Log.Debug(taskDescriptor + " returning with: " + result);
                                          });
                }
                else
                {
                    Log.Debug("Returning with: " + returnValue);
                }
            }
        }
    }
    catch (Exception ex)
    {
        if (Log.IsErrorEnabled) Log.Error(CreateInvocationLogString("ERROR", invocation), ex);
        throw;
    }
}
17

以下の一般的でクリーンなソリューションで明確にしようとしています。

  • 継続タスクとしてカスタムコードを追加するasyncメソッドをインターセプトします。

最善の解決策は、dynamicキーワードを使用してコンパイラの型チェックをバイパスし、実行時にTaskとTask<T>の違いを解決することだと思います。

public void Intercept(IInvocation invocation)
{
    invocation.Proceed();
    var method = invocation.MethodInvocationTarget;
    var isAsync = method.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null;
    if (isAsync && typeof(Task).IsAssignableFrom(method.ReturnType))
    {
        invocation.ReturnValue = InterceptAsync((dynamic)invocation.ReturnValue);
    }
}

private static async Task InterceptAsync(Task task)
{
    await task.ConfigureAwait(false);
    // do the logging here, as continuation work for Task...
}

private static async Task<T> InterceptAsync<T>(Task<T> task)
{
    T result = await task.ConfigureAwait(false);
    // do the logging here, as continuation work for Task<T>...
    return result;
}
6
thepirat000

私の2セント:

asyncメソッドの場合、インターセプターの目的は、呼び出しによって、継続を介して返されるタスクを「拡張」するであることが正しく確立されています。

さて、正確にこのタスクの継続が返されなければならないものですインターセプターの仕事を完了するために。

したがって、上記の説明と例に基づくと、これは通常のメソッドだけでなく、「生の」_async Task_メソッドでも完全に機能します。

_public virtual void Intercept(IInvocation invocation)
{
    try
    {
        invocation.Proceed();
        var task = invocation.ReturnValue as Task;
        if (task != null)
        {
            invocation.ReturnValue = task.ContinueWith(t => {
                if (t.IsFaulted)
                    OnException(invocation, t.Exception);
            });
        }
    }
    catch (Exception ex)
    {
        OnException(invocation, ex);
    }
}

public virtual void OnException(IInvocation invocation, Exception exception)
{
    ...
}
_
  1. ただし、_async Task<T>_メソッドを処理する場合、上記では、インターセプトによって返されるタスクのタイプが_Task<T>_から通常のTaskに誤って変更されます。

  2. 呼び出したいメソッドであるTask.ContinueWith()ではなくTask<TResult>.ContinueWith()を呼び出していることに注意してください。

これは、最終的にそのような傍受を待つ場合に生じる例外です。

System.InvalidCastException:タイプ 'System.Threading.Tasks.ContinuationTaskFromTask'のオブジェクトをタイプ 'System.Threading.Tasks.Task`1にキャストできません

4

以下は、非同期メソッドを正しく処理する非同期インターセプターアダプターの実装です。

abstract class AsyncInterceptor : IInterceptor
{
    class TaskCompletionSourceMethodMarkerAttribute : Attribute
    {

    }

    private static readonly MethodInfo _taskCompletionSourceMethod = typeof(AsyncInterceptor)
        .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
        .Single(x => x.GetCustomAttributes(typeof(TaskCompletionSourceMethodMarkerAttribute)).Any());


    protected virtual Task<Object> InterceptAsync(Object target, MethodBase method, object[] arguments, Func<Task<Object>> proceed)
    {
        return proceed();
    }

    protected virtual void Intercept(Object target, MethodBase method, object[] arguments, Action proceed)
    {
        proceed();
    }

    [TaskCompletionSourceMethodMarker]
    Task<TResult> TaskCompletionSource<TResult>(IInvocation invocation)
    {
        var tcs = new TaskCompletionSource<TResult>();

        var task = InterceptAsync(invocation.InvocationTarget, invocation.Method, invocation.Arguments, () =>
        {
            var task2 = (Task)invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments);
            var tcs2 = new TaskCompletionSource<Object>();
            task2.ContinueWith(x =>
            {
                if (x.IsFaulted)
                {
                    tcs2.SetException(x.Exception);
                    return;
                }
                dynamic dynamicTask = task2;
                Object result = dynamicTask.Result;
                tcs2.SetResult(result);
            });
            return tcs2.Task;
        });

        task.ContinueWith(x =>
        {
            if (x.IsFaulted)
            {
                tcs.SetException(x.Exception);
                return;
            }

            tcs.SetResult((TResult)x.Result);
        });

        return tcs.Task;
    }
    void IInterceptor.Intercept(IInvocation invocation)
    {
        if (!typeof(Task).IsAssignableFrom(invocation.Method.ReturnType))
        {
            Intercept(invocation.InvocationTarget, invocation.Method, invocation.Arguments, invocation.Proceed);
            return;
        }
        var returnType = invocation.Method.ReturnType.IsGenericType ? invocation.Method.ReturnType.GetGenericArguments()[0] : typeof(object);
        var method = _taskCompletionSourceMethod.MakeGenericMethod(returnType);
        invocation.ReturnValue = method.Invoke(this, new object[] { invocation });
    }
}

およびサンプルの使用法:

class TestInterceptor : AsyncInterceptor
{
    protected override async Task<Object> InterceptAsync(object target, MethodBase method, object[] arguments, Func<Task<object>> proceed)
    {
        await Task.Delay(5000);
        var result = await proceed();
        return DateTime.Now.Ticks % 2 == 0 ? 10000 :result;
    }
}
4
zielu1

Task<TResult>を返すメソッドをインターセプトする必要があるため、プロセスを簡素化するCastle.Coreの拡張機能を作成しました。

Castle.Core.AsyncInterceptor

パッケージは NuGet でダウンロードできます。

解決策は主にこれに基づいています answer from @ silas-reinagel ですが、実装するための新しいインターフェイスを提供することで簡素化しています IAsyncInterceptorInterceptorの実装と同様に、インターセプトを作成するためのさらなる抽象化もあります。

詳細については、プロジェクトの readme を参照してください。

1
James Skimming
   void IInterceptor.Intercept(IInvocation invocation) {
       try {
           invocation.Proceed();
           var task = invocation.ReturnValue as Task;
           if (task != null && task.IsFaulted) throw task.Exception;
       }
       catch {
           throw;
       }
   }
0
Patrick Coelho