Fody Async MethodDecorator for Exception Handling

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

So, I added a mandatory 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 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 great for synchronous code. But for asynchronous code, the exception handler is skipped, although all other IMethodDecorator (for example, OnExit and OnTaskContinuation ) is also OnTaskContinuation .

For example, considering 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 ShouldGetErrorAsync generate 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 intercepts the call and wraps the method body in a try catch. But if the method is asynchronous, it never gets into the catch statement, although fodyError.OnTaskContinuation(task) and fodyError.OnExit() are still called.

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

My question is, how should Fody generate an IL to correctly enter the error block and make sure that asynchronous errors are caught?

Here is a repo with tests that reproduce the problem

+5
source share
2 answers

You just put try-catch around the contents of the kick-off method, this will protect you only until it needs to be rescheduled first (the kick-off method will end when the asynchronous method must first reschedule, and therefore will not be in stack when the async method resumes).

Instead, you should look at a modification of the method that implements IAsyncStateMachine.MoveNext() on the destination machine. In particular, find the SetException(Exception) call in the async method AsyncVoidMethodBuilder ( AsyncVoidMethodBuilder , AsyncTaskMethodBuilder or AsyncTaskMethodBuilder<TResult> ) and end the exception just before passing it.

+1
source

await sure that asynchronous methods look simple, right? :) You just found a leak in this abstraction - the method usually returns as soon as the first await is found and your exception helper cannot catch any subsequent exceptions.

What you need to do is implement both OnException and handle the return value from the method. When the method returns and the task is not completed, you need to complete the continuation of the error in the task, which should handle exceptions the way you want them to be handled. The guys from Fody thought about this - what is the OnTaskContinuation . You need to check Task.Exception to see if there is any exception hiding in the task and handle it, however you need to.

I think this will only work if you want to throw an exception while logging or something like that - this does not allow you to replace the exception with something else. You should check it out :)

+1
source

Source: https://habr.com/ru/post/1241952/


All Articles