2016-01-31 43 views
5

Sto cercando di utilizzare Fody per avvolgere tutte le eccezioni generate da un metodo con un formato di eccezione comune.Method Fode AsyncDecorator per gestire le eccezioni

Così ho aggiunto l'attuazione necessaria dichiarazione di interfaccia e la classe che assomiglia a questo:

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

E l'attuazione del dominio che assomiglia a questo

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

    } 
    } 
} 

Questo funziona bene per il codice sincrono. Ma per il codice asincrono il gestore di eccezioni viene saltato, anche se vengono eseguiti tutti gli altri IMethodDecorator (come OnExit e OnTaskContinuation).

Per esempio, guardando la seguente classe di test:

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

vedo che ShouldGetErrorAsync produce il seguente codice 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; 
} 

E ShouldGetErrorAsync2 genera:

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

Se Chiamo ShouldGetErrorAsync, Fody è intercetta chiamata e avvolgendo il corpo del metodo in un tentativo di cattura. Ma se il metodo è asincrono, non raggiunge mai l'istruzione catch anche se sono ancora chiamati i numeri fodyError.OnTaskContinuation(task) e fodyError.OnExit().

D'altra parte, ShouldGetErrorAsync gestirà l'errore correttamente, anche se non vi è alcun blocco di gestione degli errori nell'IL.

La mia domanda è, come dovrebbe Fody generare l'IL per iniettare correttamente il blocco di errore e renderlo così intercettato dagli errori asincroni?

Here is a repo with tests that reproduces the issue

risposta

1

si sta mettendo solo il try-catch attorno al contenuto del metodo 'calcio d'inizio', questo proteggerà soltanto fino al punto in cui si deve prima di riprogrammare (il 'calcio d'inizio 'il metodo terminerà quando il metodo asincrono deve prima riprogrammare e quindi non sarà in pila quando riprende il metodo asincrono.

Si consiglia di modificare il metodo di implementazione IAsyncStateMachine.MoveNext() sulla macchina a stati. In particolare, cercare la chiamata al SetException(Exception) sul costruttore di metodo asincrono (AsyncVoidMethodBuilder, AsyncTaskMethodBuilder o AsyncTaskMethodBuilder<TResult>) e avvolgere l'eccezione poco prima di passare in.

+0

Questo è tecnicamente corretto, ma è stato difficile da implementare. Alla fine ho cambiato le librerie in https://github.com/vescon/MethodBoundaryAspect.Fody. Questo gestisce i problemi asincroni e ho trovato il progetto più facile da lavorare e modificare. – swestner

1

await si assicura metodi asincroni sembrano semplici, non è vero? :) Hai appena trovato una fuga in quell'astrazione: il metodo di solito ritorna non appena viene trovato il primo await e l'helper delle eccezioni non ha modo di intercettare eventuali eccezioni successive.

Quello che dovete fare è implementare sia il OnException, e gestire il valore restituito dal metodo. Quando il metodo restituisce e l'attività non è completata, è necessario arrestare una continuazione di errore sull'attività, che deve gestire le eccezioni nel modo in cui si desidera che vengano gestite. I ragazzi di Fody ci hanno pensato - questo è il motivo per cui è lo OnTaskContinuation. È necessario controllare lo Task.Exception per vedere se c'è un'eccezione in agguato nell'attività e gestirla, comunque è necessario.

Penso che funzionerà solo se si desidera rilanciare l'eccezione durante la registrazione o qualcosa del genere - non consente di sostituire l'eccezione con qualcosa di diverso. Dovresti testarlo :)

+0

Sfortunatamente sei corretto, l'errore può essere segnalato e non gestito. – swestner