关于 c#:Fody Async MethodDecorator 处理异常

Fody Async MethodDecorator to Handle Exceptions

我正在尝试使用 Fody 来package从具有通用异常格式的方法中抛出的所有异常。

所以我添加了所需的接口声明和类实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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");
  }
}

以及看起来像这样的域实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 都已执行(如 OnExitOnTaskContinuation)。

例如,看下面的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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 生成:

1
2
3
4
5
6
7
8
9
10
11
12
    // 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 会拦截调用,并将方法体package在 try catch 中。但是如果方法是异步的,即使 fodyError.OnTaskContinuation(task)fodyError.OnExit() 仍然被调用,它也永远不会命中 catch 语句。

另一方面,ShouldGetErrorAsync 将很好地处理错误,即使 IL 中没有错误处理块。

我的问题是,Fody 应该如何生成 IL 以正确注入错误块并使其拦截异步错误?

这是一个包含重现问题的测试的 repo


await 确实让异步方法看起来很简单,不是吗? :) 您刚刚在该抽象中发现了一个泄漏 - 该方法通常在找到第一个 await 后立即返回,并且您的异常助手无法拦截任何以后的异常。

你需要做的是实现OnException,并处理方法的返回值。当方法返回并且任务未完成时,您需要结束任务的错误延续,这需要按照您希望的方式处理异常。 Fody 的人想到了这一点——这就是 OnTaskContinuation 的用途。您需要检查 Task.Exception 以查看任务中是否存在异常,并根据需要进行处理。

我认为这仅在您想在进行日志记录或其他操作时重新抛出异常时才有效 - 它不允许您用不同的东西替换异常。你应该测试一下:)


您只是将 try-catch 放置在 'kick-off' 方法的内容周围,这只会保护您,直到它首先需要重新安排('kick-off' 方法将在async 方法首先需要重新调度,因此在 async 方法恢复时不会在堆栈上。

您应该考虑修改在状态机上实现 IAsyncStateMachine.MoveNext() 的方法。特别是,在异步方法构建器(AsyncVoidMethodBuilderAsyncTaskMethodBuilderAsyncTaskMethodBuilder<TResult>)上查找对 SetException(Exception) 的调用,并在将异常传入之前将其package起来。