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