Fody Async MethodDecorator для обработки исключений

Я пытаюсь использовать Fody для переноса всех исключений, созданных из метода, в общий формат исключений.

Поэтому я добавил требуемое объявление интерфейса и реализацию класса, которая выглядит следующим образом:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;

[module: MethodDecorator]

public interface IMethodDecorator
{
  void Init(object instance, MethodBase method, object[] args);
  void OnEntry();
  void OnExit();
  void OnException(Exception exception);
  void OnTaskContinuation(Task t);
}


[AttributeUsage(
    AttributeTargets.Module |
    AttributeTargets.Method |
    AttributeTargets.Assembly |
    AttributeTargets.Constructor, AllowMultiple = true)]
public class MethodDecorator : Attribute, IMethodDecorator
{
  public virtual void Init(object instance, MethodBase method, object[] args) { }

  public void OnEntry()
  {
    Debug.WriteLine("base on entry");
  }

  public virtual void OnException(Exception exception)
  {
    Debug.WriteLine("base on exception");
  }

  public void OnExit()
  {
    Debug.WriteLine("base on exit");
  }

  public void OnTaskContinuation(Task t)
  {
    Debug.WriteLine("base on continue");
  }
}

И реализация домена, которая выглядит так

using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;

namespace CC.Spikes.AOP.Fody
{
  public class FodyError : MethodDecorator
  {
    public string TranslationKey { get; set; }
    public Type ExceptionType { get; set; }

    public override void Init(object instance, MethodBase method, object[] args)
    {
      SetProperties(method);
    }

    private void SetProperties(MethodBase method)
    {
      var attribute = method.CustomAttributes.First(n => n.AttributeType.Name == nameof(FodyError));
      var translation = attribute
        .NamedArguments
        .First(n => n.MemberName == nameof(TranslationKey))
        .TypedValue
        .Value
          as string;

      var exceptionType = attribute
        .NamedArguments
        .First(n => n.MemberName == nameof(ExceptionType))
        .TypedValue
        .Value
          as Type;


      TranslationKey = translation;
      ExceptionType = exceptionType;
    }

    public override void OnException(Exception exception)
    {
      Debug.WriteLine("entering fody error exception");
      if (exception.GetType() != ExceptionType)
      {
        Debug.WriteLine("rethrowing fody error exception");
        //rethrow without losing stacktrace
        ExceptionDispatchInfo.Capture(exception).Throw();
      }

      Debug.WriteLine("creating new fody error exception");
      throw new FodyDangerException(TranslationKey, exception);

    }
  }

  public class FodyDangerException : Exception
  {
    public string CallState { get; set; }
    public FodyDangerException(string message, Exception error) : base(message, error)
    {

    }
  }
}

Это отлично работает для синхронного кода. Но для асинхронного кода обработчик исключений пропускается, даже если выполняются все остальные IMethodDecorator (например, OnExit и OnTaskContinuation).

Например, глядя на следующий тестовый класс:

public class FodyTestStub
{ 

  [FodyError(ExceptionType = typeof(NullReferenceException), TranslationKey = "EN_WHATEVER")]
  public async Task ShouldGetErrorAsync()
  {
    await Task.Delay(200);
    throw new NullReferenceException();
  }

  public async Task ShouldGetErrorAsync2()
  {
    await Task.Delay(200);
    throw new NullReferenceException();
  }
}

Я вижу, что ShouldGetErrorAsync создает следующий код IL:

// CC.Spikes.AOP.Fody.FodyTestStub
[FodyError(ExceptionType = typeof(NullReferenceException), TranslationKey = "EN_WHATEVER"), DebuggerStepThrough, AsyncStateMachine(typeof(FodyTestStub.<ShouldGetErrorAsync>d__3))]
public Task ShouldGetErrorAsync()
{
    MethodBase methodFromHandle = MethodBase.GetMethodFromHandle(methodof(FodyTestStub.ShouldGetErrorAsync()).MethodHandle, typeof(FodyTestStub).TypeHandle);
    FodyError fodyError = (FodyError)Activator.CreateInstance(typeof(FodyError));
    object[] args = new object[0];
    fodyError.Init(this, methodFromHandle, args);
    fodyError.OnEntry();
    Task task;
    try
    {
        FodyTestStub.<ShouldGetErrorAsync>d__3 <ShouldGetErrorAsync>d__ = new FodyTestStub.<ShouldGetErrorAsync>d__3();
        <ShouldGetErrorAsync>d__.<>4__this = this;
        <ShouldGetErrorAsync>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
        <ShouldGetErrorAsync>d__.<>1__state = -1;
        AsyncTaskMethodBuilder <>t__builder = <ShouldGetErrorAsync>d__.<>t__builder;
        <>t__builder.Start<FodyTestStub.<ShouldGetErrorAsync>d__3>(ref <ShouldGetErrorAsync>d__);
        task = <ShouldGetErrorAsync>d__.<>t__builder.Task;
        fodyError.OnExit();
    }
    catch (Exception exception)
    {
        fodyError.OnException(exception);
        throw;
    }
    return task;
}

И ShouldGetErrorAsync2 генерирует:

    // CC.Spikes.AOP.Fody.FodyTestStub
[DebuggerStepThrough, AsyncStateMachine(typeof(FodyTestStub.<ShouldGetErrorAsync2>d__4))]
public Task ShouldGetErrorAsync2()
{
    FodyTestStub.<ShouldGetErrorAsync2>d__4 <ShouldGetErrorAsync2>d__ = new FodyTestStub.<ShouldGetErrorAsync2>d__4();
    <ShouldGetErrorAsync2>d__.<>4__this = this;
    <ShouldGetErrorAsync2>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
    <ShouldGetErrorAsync2>d__.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = <ShouldGetErrorAsync2>d__.<>t__builder;
    <>t__builder.Start<FodyTestStub.<ShouldGetErrorAsync2>d__4>(ref <ShouldGetErrorAsync2>d__);
    return <ShouldGetErrorAsync2>d__.<>t__builder.Task;
}

Если я вызываю ShouldGetErrorAsync, Fody перехватывает вызов и оборачивает тело метода в try catch. Но если метод асинхронный, он никогда не попадет в оператор catch, даже если fodyError.OnTaskContinuation(task) и fodyError.OnExit() все еще вызываются.

С другой стороны, ShouldGetErrorAsync прекрасно обработает ошибку, даже если в IL нет блока обработки ошибок.

Мой вопрос: как Фоди должен генерировать IL, чтобы правильно вводить блок ошибок и делать так, чтобы асинхронные ошибки перехватывались?

Вот репозиторий с тестами, воспроизводящими проблему


person swestner    schedule 31.01.2016    source источник


Ответы (2)


Вы только помещаете try-catch вокруг содержимого метода «kick-off», это защитит вас только до того момента, когда он сначала должен быть перепланирован (метод «kick-off» завершится, когда асинхронный метод сначала необходимо перепланировать, и поэтому он не будет находиться в стеке при возобновлении асинхронного метода).

Вместо этого вам следует изменить метод, реализующий IAsyncStateMachine.MoveNext() в конечном автомате. В частности, ищите вызов SetException(Exception) в конструкторе асинхронных методов (AsyncVoidMethodBuilder, AsyncTaskMethodBuilder или AsyncTaskMethodBuilder<TResult>) и обертывайте исключение непосредственно перед его передачей.

person Brian Reichle    schedule 01.02.2016
comment
Это технически правильно, но было сложно реализовать. В конце концов я переключил библиотеки на github.com/vescon/MethodBoundaryAspect.Fody. Это решает проблемы с асинхронностью, и я обнаружил, что с проектом легче работать и изменять его. - person swestner; 05.02.2016

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

Что вам нужно сделать, так это реализовать как OnException, так и обработать возвращаемое значение из метода. Когда метод возвращается, а задача не завершена, вам нужно завершить продолжение ошибки в задаче, которая должна обрабатывать исключения так, как вы хотите, чтобы они обрабатывались. Ребята из Fody подумали об этом - для этого и существует OnTaskContinuation. Вам нужно проверить Task.Exception, чтобы увидеть, не скрывается ли в задаче исключение, и обработать его, как вам нужно.

Я думаю, что это будет работать только в том случае, если вы хотите повторно сгенерировать исключение во время ведения журнала или чего-то еще - это не позволяет вам заменить исключение чем-то другим. Вы должны проверить это :)

person Luaan    schedule 01.02.2016
comment
К сожалению, вы правы, об ошибке можно только сообщить, а не переработать. - person swestner; 02.02.2016