2016-01-31 31 views
5

Ich versuche, Fody zu verwenden, um alle Ausnahmen, die von einer Methode mit einem allgemeinen Ausnahmeformat ausgelöst werden, zu umbrechen.Fody Async MethodDecorator zur Behandlung von Ausnahmen

So habe ich die erforderliche Interface-Deklaration und Klassenimplementierung hinzugefügt, die wie folgt aussieht: Dies funktioniert gut für synchronen Code

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

und die Domain-Implementierung, die

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

    } 
    } 
} 

wie diese aussieht. Aber für asynchronen Code wird der Exception-Handler übersprungen, obwohl alle anderen IMethodDecorator ausgeführt werden (wie OnExit und OnTaskContinuation).

Zum Beispiel unter der folgenden Testklasse suchen:

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

Ich sehe, dass ShouldGetErrorAsync den folgenden IL-Code erzeugt:

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

Und ShouldGetErrorAsync2 erzeugt:

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

Wenn Ich rufe ShouldGetErrorAsync, Fody ist abfangen den Aufruf und das Einbetten des Methodenrumpfs in einen try catch. Wenn die Methode jedoch async ist, trifft sie die catch-Anweisung nie, obwohl die fodyError.OnTaskContinuation(task) und fodyError.OnExit() immer noch aufgerufen werden.

Auf der anderen Seite wird ShouldGetErrorAsync den Fehler gut behandeln, obwohl es keinen Fehlerbehandlungsblock in der IL gibt.

Meine Frage ist, wie sollte Fody die IL erzeugen, um den Fehlerblock richtig zu injizieren und es so zu machen, dass asynchrone Fehler abgefangen werden?

Here is a repo with tests that reproduces the issue

Antwort

1

Sie nur den try-catch um den Inhalt des ‚Kick-off‘ -Methode platzieren, dieses Sie nur bis zu dem Punkt schützen, wo es muss zuerst neu planen (die ‚Kick-off 'Die Methode endet, wenn die Async-Methode erst neu geplant werden muss und sich daher nicht auf dem Stapel befindet, wenn die Async-Methode fortgesetzt wird.

Sie sollten die Methode, die IAsyncStateMachine.MoveNext() implementiert, stattdessen auf der Statusmaschine ändern. Insbesondere sucht den Anruf SetException(Exception) auf der Asynchron-Methode Builder (AsyncVoidMethodBuilder, AsyncTaskMethodBuilder oder AsyncTaskMethodBuilder<TResult>) und wickeln Sie die Ausnahme, kurz bevor es in vorbei.

+0

Das ist technisch korrekt, aber war schwierig zu implementieren. Am Ende habe ich Bibliotheken auf https://github.com/vescon/MethodBoundaryAspect.Fody umgestellt. Dies behandelt die asynchronen Probleme und ich fand das Projekt einfacher zu bearbeiten und zu ändern. – swestner

1

await stellt sicher, asynchrone Methoden einfach aussehen, nicht wahr? :) Sie haben gerade ein Leck in dieser Abstraktion gefunden - die Methode kehrt normalerweise zurück, sobald die erste await gefunden wurde, und Ihr Exception-Helfer hat keine Möglichkeit, irgendwelche späteren Ausnahmen abzufangen.

Was Sie tun müssen, ist implementieren Sie die OnException, und behandeln Sie den Rückgabewert von der Methode. Wenn die Methode zurückkehrt und die Aufgabe nicht abgeschlossen ist, müssen Sie eine Fehlerfortsetzung für die Aufgabe erstellen, die die Ausnahmen so behandelt, wie Sie sie behandeln möchten. Die Jungs von Fody haben daran gedacht - dafür steht die OnTaskContinuation. Sie müssen die Task.Exception überprüfen, um zu sehen, ob eine Ausnahme in der Aufgabe lauert, und sie behandeln, wie auch immer Sie es brauchen.

Ich denke, das wird nur funktionieren, wenn Sie die Ausnahme während der Protokollierung oder etwas wiederholen möchten - es erlaubt Ihnen nicht, die Ausnahme durch etwas anderes zu ersetzen. Sie sollten das testen :)

+0

Leider sind Sie richtig, der Fehler kann nur gemeldet und nicht erneut behandelt werden. – swestner