Подтвердить что ты не робот

Перехватить вызов метода async с помощью DynamicProxy

Ниже приведен код из метода Intercept для настраиваемого типа, который реализует IInterceptor библиотеки Castle Dynamic Proxy. Этот фрагмент представлен из 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 (используя ключевые слова async/await из С# 5.0). И я верю, я понимаю причины этого.

Для работы 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 вернулся туда, а не исходный вызывающий?

4b9b3361

Ответ 1

Предположительно, "проблема" заключается в том, что он просто регистрирует, что он возвращает задачу, - и вам нужно значение внутри этой задачи?

Предполагая, что в этом случае вам все равно нужно вернуть задачу вызывающему, немедленно - не дожидаясь завершения. Если вы нарушите это, вы в корне запутались.

Однако перед возвратом задачи вызывающему абоненту вы должны добавить продолжение (через Task.ContinueWith), который будет регистрировать результат (или сбой), когда задача завершена. Это все равно даст информацию о результатах, но, конечно же, вы будете регистрировать ее потенциально после некоторых других протоколов. Возможно, вам также захочется войти в систему сразу перед возвратом, что приведет к журналу примерно так:

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

Бизнес, связанный с получением результата из задачи (если он завершен успешно), должен быть выполнен с отражением, что немного больно - или вы можете использовать динамическую типизацию. (В любом случае это будет немного хитом производительности.)

Ответ 2

Спасибо Jon, вот что я закончил с:

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

Ответ 3

Попытка уточнить с помощью общего и чистого решения для:

  • Перехват методов 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;
}

Ответ 4

Мои 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)
{
    ...
}
  • Но при работе с методами async Task<T> вышеизложенное неправильно изменило бы тип задачи, возвращаемой перехватом, от Task<T> до регулярного Task

  • Обратите внимание, что мы вызываем Task.ContinueWith(), а не Task<TResult>.ContinueWith(), который является методом, который мы хотим вызвать.

Это будет результирующее исключение, которое в конечном итоге ожидает такого перехвата:

System.InvalidCastException: невозможно выполнить листинг объекта типа "System.Threading.Tasks.ContinuationTaskFromTask" для ввода "System.Threading.Tasks.Task`1

Ответ 5

Ниже приведена реализация адаптера адаптера перехватчика async, которая корректно обрабатывает методы async.

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

Ответ 6

Имея необходимость перехватывать методы, возвращающие Task<TResult>, я создал расширение для Castle.Core, что упрощает процесс.

Castle.Core.AsyncInterceptor

Пакет доступен для загрузки на NuGet.

Решение в значительной степени основано на этом ответе от @silas-reinagel, но упрощает его, предоставляя новый интерфейс для реализации IAsyncInterceptor. Существуют также дополнительные абстракции, которые делают перехват похожим на реализацию Interceptor.

Подробнее см. readme проекта.

Ответ 7

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

Ответ 8

Вместо:

tcs2.SetException(x.Exception);

Вы должны использовать:

x.Exception.Handle(ex => { tcs2.SetException(ex); return true; });

чтобы создать реальное исключение...