Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fody Async MethodDecorator to Handle Exceptions

I am trying to use Fody to wrap all exceptions thrown from a method with a common exception format.

So I have added the required interface declaration and class implementation that looks like this :

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

And the domain implementation that looks like this

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)
    {

    }
  }
}

This works fine for synchronous code. But for asynchronous code the exception handler is skipped, even though all the other IMethodDecorator are executed (like OnExit, and OnTaskContinuation).

For example, looking at the following test class :

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

I see that ShouldGetErrorAsync produces the following IL code :

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

And ShouldGetErrorAsync2 generates :

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

If I call ShouldGetErrorAsync, Fody is intercepting the call, and wrapping the method body in a try catch. But if the method is async, it never hits the catch statement even though the fodyError.OnTaskContinuation(task) and fodyError.OnExit() are still called.

On the other hand, ShouldGetErrorAsync will handle the error just fine, even though there is no error handling block in the IL.

My question is, how should Fody be generating the IL to properly inject the error block and make it so async errors are intercepted?

Here is a repo with tests that reproduces the issue

like image 402
swestner Avatar asked Jan 31 '16 22:01

swestner


1 Answers

You are only placing the try-catch around the content of the 'kick-off' method, this will only protect you up to the point where it first needs to reschedule (the 'kick-off' method will end when the async method first needs to reschedule and so will not be on the stack when the async method resumes).

You should look at modifying the method implementing IAsyncStateMachine.MoveNext() on the state machine instead. In particular, look for the call to SetException(Exception) on the async method builder (AsyncVoidMethodBuilder, AsyncTaskMethodBuilder or AsyncTaskMethodBuilder<TResult>) and wrap the exception just before passing it in.

like image 101
Brian Reichle Avatar answered Sep 21 '22 17:09

Brian Reichle