2016-01-31 39 views
5

Próbuję użyć Fody do zawijania wszystkich wyjątków generowanych przez metodę z typowym formatem wyjątków.Fody Async MethodDecorator do obsługi wyjątków

Więc dodałem wymaganej deklaracji interfejsu i implementacji klasy, który wygląda tak:

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

i wdrożenie domeny, który wygląda tak

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

    } 
    } 
} 

Działa to dobrze dla kodu synchronicznego. Ale w przypadku kodu asynchronicznego procedura obsługi wyjątku jest pomijana, mimo że wszystkie inne IMethodDecorator są wykonywane (jak OnExit i OnTaskContinuation).

Na przykład, patrząc na poniższym klasie testu:

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

widzę, że ShouldGetErrorAsync produkuje następujący kod 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; 
} 

I ShouldGetErrorAsync2 generuje:

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

Jeśli Dzwonię pod numer ShouldGetErrorAsync, przechwytuje Fody wywołanie i zawijanie treści metody w próbie catch. Ale jeśli metoda jest asynchroniczna, nigdy nie trafi do instrukcji catch, nawet jeśli fodyError.OnTaskContinuation(task) i fodyError.OnExit() są nadal wywoływane.

Z drugiej strony, ShouldGetErrorAsync poradzi sobie z błędem, nawet jeśli w IL nie ma bloku obsługi błędów.

Moje pytanie brzmi: w jaki sposób Fody powinien generować IL, aby odpowiednio wstrzyknąć blok błędu i uczynić go tak, aby przechwycić błędy asynchroniczne?

Here is a repo with tests that reproduces the issue

Odpowiedz

1

zostały umieszczone jedynie w try-catch wokół treści metodą „kick-off”, to tylko cię chronić aż do punktu, w którym musi najpierw przełożyć (z „Kick-off "metoda zakończy się, gdy metoda asynchroniczna musi najpierw zostać przełożona, a więc nie będzie na stosie, gdy wznowi się metoda asynchroniczna).

Zamiast tego należy spojrzeć na modyfikację metody implementującej IAsyncStateMachine.MoveNext() na komputerze stanu. W szczególności należy zwrócić uwagę na wezwanie do SetException(Exception) na budowniczego metody asynchronicznej (AsyncVoidMethodBuilder, AsyncTaskMethodBuilder lub AsyncTaskMethodBuilder<TResult>) i owinąć wyjątek tuż przed przekazaniem go w.

+0

Jest to technicznie poprawne, ale było trudne do wdrożenia. W końcu przełączyłem biblioteki na https://github.com/vescon/MethodBoundaryAspect.Fody. Rozwiązuje to problemy asynchroniczne i uznałem, że łatwiej jest pracować z projektem i modyfikować go. – swestner

1

await na pewno sprawia, że ​​metody asynchroniczne wyglądać proste, prawda? :) Właśnie znalazłeś przeciek w tej abstrakcji - metoda zwykle powraca, gdy tylko zostanie znalezione pierwsze await, a twój pomocnik wyjątku nie ma możliwości przechwycenia żadnych późniejszych wyjątków.

To, co musisz zrobić, to zaimplementować zarówno OnException, jak i obsłużyć wartość zwracaną przez metodę. Gdy metoda wraca, a zadanie nie jest zakończone, należy zakończyć działanie błędu w zadaniu, które musi obsługiwać wyjątki w taki sposób, w jaki mają być obsługiwane. Faceci z Fody myśleli o tym - do tego właśnie służy OnTaskContinuation. Musisz sprawdzić numer Task.Exception, aby sprawdzić, czy w zadaniu występuje wyjątek i obsłużyć go, jakkolwiek musisz.

Myślę, że to zadziała tylko wtedy, gdy użytkownik chce ponownie zgłosić wyjątek podczas rejestrowania lub coś takiego - nie pozwala na zastąpienie wyjątku czymś innym. Powinieneś to sprawdzić :)

+0

Niestety, masz rację, błąd można zgłosić tylko wtedy, gdy nie jest on ponownie używany. – swestner