¿Cómo volver a lanzar InnerException sin perder el seguimiento de la pila en C #?

305

Estoy llamando, a través de la reflexión, un método que puede causar una excepción. ¿Cómo puedo pasar la excepción a mi interlocutor sin que el reflejo del contenedor lo coloque?
Estoy volviendo a lanzar la InnerException, pero esto destruye la traza de la pila.
Código de ejemplo:

public void test1()
{
    // Throw an exception for testing purposes
    throw new ArgumentException("test1");
}

void test2()
{
    try
    {
        MethodInfo mi = typeof(Program).GetMethod("test1");
        mi.Invoke(this, null);
    }
    catch (TargetInvocationException tiex)
    {
        // Throw the new exception
        throw tiex.InnerException;
    }
}
Skolima
fuente
1
Hay otra forma de hacer esto que no requiere ningún vudú. Eche un vistazo a la respuesta aquí: stackoverflow.com/questions/15668334/…
Timothy Shields el
La excepción lanzada en el método llamado dinámicamente es la excepción interna de la excepción "La excepción ha sido lanzada por el destino de una invocación". Tiene su propio rastro de pila. Realmente no hay mucho más de qué preocuparse.
ajeh

Respuestas:

481

En .NET 4.5 ahora existe la ExceptionDispatchInfoclase.

Esto le permite capturar una excepción y volver a lanzarla sin cambiar el seguimiento de la pila:

try
{
    task.Wait();
}
catch(AggregateException ex)
{
    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}

Esto funciona en cualquier excepción, no solo AggregateException.

Se introdujo debido a la awaitfunción de lenguaje C #, que desenvuelve las excepciones internas de las AggregateExceptioninstancias para hacer que las funciones de lenguaje asíncrono se parezcan más a las funciones de lenguaje síncrono.

Paul Turner
fuente
11
¿Buen candidato para un método de extensión Exception.Rethrow ()?
nmarler
8
Tenga en cuenta que la clase ExceptionDispatchInfo está en el espacio de nombres System.Runtime.ExceptionServices y no está disponible antes de .NET 4.5.
Yoyo
53
Es posible que necesite poner un regular throw;después de la línea .Throw (), porque el compilador no sabrá que .Throw () siempre arroja una excepción. throw;nunca se llamará como resultado, pero al menos el compilador no se quejará si su método requiere un objeto de retorno o es una función asíncrona.
Todd
55
@Taudris Esta pregunta se refiere específicamente a volver a lanzar la excepción interna, que no puede ser manejada especialmente por throw;. Si usa throw ex.InnerException;el stack-trace se reinicia en el punto en que se vuelve a lanzar.
Paul Turner
55
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran
86

Que es posible preservar el seguimiento de la pila antes de Regeneración de sin reflexión:

static void PreserveStackTrace (Exception e)
{
    var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ;
    var mgr = new ObjectManager     (null, ctx) ;
    var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ;

    e.GetObjectData    (si, ctx)  ;
    mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData
    mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData

    // voila, e is unmodified save for _remoteStackTraceString
}

Esto desperdicia muchos ciclos en comparación con las llamadas a InternalPreserveStackTracetravés de un delegado en caché, pero tiene la ventaja de depender solo de la funcionalidad pública. Aquí hay un par de patrones de uso comunes para las funciones de preservación de seguimiento de pila:

// usage (A): cross-thread invoke, messaging, custom task schedulers etc.
catch (Exception e)
{
    PreserveStackTrace (e) ;

    // store exception to be re-thrown later,
    // possibly in a different thread
    operationResult.Exception = e ;
}

// usage (B): after calling MethodInfo.Invoke() and the like
catch (TargetInvocationException tiex)
{
    PreserveStackTrace (tiex.InnerException) ;

    // unwrap TargetInvocationException, so that typed catch clauses 
    // in library/3rd-party code can work correctly;
    // new stack trace is appended to existing one
    throw tiex.InnerException ;
}
Anton Tykhyy
fuente
Parece genial, ¿qué debe suceder después de ejecutar estas funciones?
vdboor
2
En realidad, no es mucho más lento que invocar InternalPreserveStackTrace(aproximadamente un 6% más lento con 10000 iteraciones). Acceder a los campos directamente por reflexión es aproximadamente un 2,5% más rápido que invocarloInternalPreserveStackTrace
Thomas Levesque
1
Recomendaría usar el e.Datadiccionario con una cadena o una clave de objeto única ( static readonly object myExceptionDataKey = new object ()pero no haga esto si tiene que serializar excepciones en cualquier lugar). Evite modificar e.Message, porque podría tener código en algún lugar que analiza e.Message. El análisis e.Messagees malo, pero puede que no haya otra opción, por ejemplo, si tiene que usar una biblioteca de terceros con prácticas de excepción deficientes.
Anton Tykhyy el
10
DoFixups se rompe para excepciones personalizadas si no tienen el ctor de serialización
ruslander
3
La solución sugerida no funciona si la excepción no tiene un constructor de serialización. Sugiero utilizar la solución propuesta en stackoverflow.com/a/4557183/209727 que funciona bien en cualquier caso. Para .NET 4.5 considere usar la clase ExceptionDispatchInfo.
Davide Icardi
33

Creo que su mejor apuesta sería simplemente poner esto en su bloque de captura:

throw;

Y luego extraiga la innerexcepción más tarde.

GEOCHET
fuente
21
O elimine el try / catch por completo.
Daniel Earwicker
66
@Earwicker. Eliminar el try / catch no es una buena solución en general, ya que ignora los casos en que se requiere un código de limpieza antes de propagar la excepción en la pila de llamadas.
Jordania el
12
@ Jordania - El código de limpieza debe estar en un bloque finalmente, no en un bloque de captura
Paolo el
17
@Paolo: si se supone que debe ejecutarse en todos los casos, sí. Si se supone que debe ejecutarse solo en caso de falla, no.
chiccodoro
44
Tenga en cuenta que InternalPreserveStackTrace no es seguro para subprocesos, por lo que si tiene 2 subprocesos en estos estados de excepción ... que Dios tenga piedad de todos nosotros.
Rob
14

Nadie ha explicado la diferencia entre ExceptionDispatchInfo.Capture( ex ).Throw()y una simple throw, así que aquí está.

La forma completa de volver a generar una excepción detectada es utilizarla ExceptionDispatchInfo.Capture( ex ).Throw()(solo disponible en .Net 4.5).

A continuación están los casos necesarios para probar esto:

1)

void CallingMethod()
{
    //try
    {
        throw new Exception( "TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2)

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    }
}

3)

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch
    {
        throw;
    }
}

4)

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        throw new Exception( "RETHROW", ex );
    }
}

El caso 1 y el caso 2 le darán un seguimiento de la pila donde el número de línea del código fuente para el CallingMethodmétodo es el número de línea delthrow new Exception( "TEST" ) línea línea.

Sin embargo, el caso 3 le dará un seguimiento de la pila donde el número de línea del código fuente para el CallingMethodmétodo es el número de línea de la throwllamada. Esto significa que si la throw new Exception( "TEST" )línea está rodeada por otras operaciones, no tiene idea de en qué número de línea se produjo la excepción.

El caso 4 es similar con el caso 2 porque el número de línea de la excepción original se conserva, pero no es un cambio real porque cambia el tipo de la excepción original.

jeuoekdcwzfwccu
fuente
55
Siempre pensé que 'throw' no restablecía el stacktrace (a diferencia de 'throw e').
Jesper Matthiesen
@JesperMatthiesen Podría estar equivocado, pero escuché que depende de si la excepción fue arrojada y atrapada en el mismo archivo. Si es el mismo archivo, se perderá el seguimiento de la pila, si se trata de otro archivo, se conservará.
jahu
13
public static class ExceptionHelper
{
    private static Action<Exception> _preserveInternalException;

    static ExceptionHelper()
    {
        MethodInfo preserveStackTrace = typeof( Exception ).GetMethod( "InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic );
        _preserveInternalException = (Action<Exception>)Delegate.CreateDelegate( typeof( Action<Exception> ), preserveStackTrace );            
    }

    public static void PreserveStackTrace( this Exception ex )
    {
        _preserveInternalException( ex );
    }
}

Llame al método de extensión en su excepción antes de lanzarlo, conservará el seguimiento de la pila original.

Eric
fuente
Tenga en cuenta que en .Net 4.0, InternalPreserveStackTrace ahora es un no-op - ¡mire en Reflector y verá que el método está completamente vacío!
Samuel Jack
Tacha eso: estaba mirando el RC: en la versión beta, ¡han vuelto a poner la implementación nuevamente!
Samuel Jack
3
sugerencia: cambie PreserveStackTrace para devolver ex, luego para lanzar una excepción puede decir: throw ex.PreserveStackTrace ();
Simon_Weaver
¿Por qué usar Action<Exception>? Aquí use el método estático
Kiquenet
10

Aún más reflexión ...

catch (TargetInvocationException tiex)
{
    // Get the _remoteStackTraceString of the Exception class
    FieldInfo remoteStackTraceString = typeof(Exception)
        .GetField("_remoteStackTraceString",
            BindingFlags.Instance | BindingFlags.NonPublic); // MS.Net

    if (remoteStackTraceString == null)
        remoteStackTraceString = typeof(Exception)
        .GetField("remote_stack_trace",
            BindingFlags.Instance | BindingFlags.NonPublic); // Mono

    // Set the InnerException._remoteStackTraceString
    // to the current InnerException.StackTrace
    remoteStackTraceString.SetValue(tiex.InnerException,
        tiex.InnerException.StackTrace + Environment.NewLine);

    // Throw the new exception
    throw tiex.InnerException;
}

Tenga en cuenta que esto puede romperse en cualquier momento, ya que los campos privados no son parte de la API. Ver más discusión sobre Mono bugzilla .

Skolima
fuente
28
Esta es una muy, muy mala idea, ya que depende de detalles internos no documentados sobre las clases de framework.
Daniel Earwicker
1
Resulta que es posible preservar el seguimiento de la pila sin Reflection, ver más abajo.
Anton Tykhyy
1
Llamar al InternalPreserveStackTracemétodo interno sería mejor, ya que hace lo mismo y es menos probable que cambie en el futuro ...
Thomas Levesque
1
En realidad, sería peor, ya que InternalPreserveStackTrace no existe en Mono.
skolima
55
@daniel: bueno, es una muy, muy, muy mala idea para tirar; para restablecer el stacktrace cuando cada desarrollador de .net está entrenado para creer que no lo hará. También es algo muy, muy malo si no puede encontrar la fuente de una NullReferenceException y pierde un cliente / pedido porque no puede encontrarlo. para mí eso triunfa sobre los 'detalles indocumentados' y definitivamente mono.
Simon_Weaver
10

Primero: no pierda la TargetInvocationException: es información valiosa cuando querrá depurar cosas.
Segundo: Envuelva el TIE como InnerException en su propio tipo de excepción y coloque una propiedad OriginalException que se vincule a lo que necesita (y mantenga intacta toda la pila de llamadas).
Tercero: deje que el TIE salga de su método.

kokos
fuente
5

Chicos, ustedes son geniales ... pronto seré nigromante.

    public void test1()
    {
        // Throw an exception for testing purposes
        throw new ArgumentException("test1");
    }

    void test2()
    {
            MethodInfo mi = typeof(Program).GetMethod("test1");
            ((Action)Delegate.CreateDelegate(typeof(Action), mi))();

    }
Boris Treukhov
fuente
1
Buena idea, pero no siempre controlas el código que llama .Invoke().
Anton Tykhyy
1
Y tampoco siempre conoce los tipos de argumentos / resultados en tiempo de compilación.
Roman Starkov
3

Otro código de muestra que utiliza serialización / deserialización de excepción. No requiere que el tipo de excepción real sea serializable. También utiliza solo métodos públicos / protegidos.

    static void PreserveStackTrace(Exception e)
    {
        var ctx = new StreamingContext(StreamingContextStates.CrossAppDomain);
        var si = new SerializationInfo(typeof(Exception), new FormatterConverter());
        var ctor = typeof(Exception).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext) }, null);

        e.GetObjectData(si, ctx);
        ctor.Invoke(e, new object[] { si, ctx });
    }
chickenbyproduct
fuente
no requiere que el tipo de excepción real sea serializable?
Kiquenet
3

Basado en la respuesta de Paul Turners, hice un método de extensión

    public static Exception Capture(this Exception ex)
    {
        ExceptionDispatchInfo.Capture(ex).Throw();
        return ex;
    }

return exnunca se llegó a la meta, pero la ventaja es que puedo usarlo throw ex.Capture()como un trazador de líneas para que el compilador no genere un not all code paths return a valueerror.

    public static object InvokeEx(this MethodInfo method, object obj, object[] parameters)
    {
        {
            return method.Invoke(obj, parameters);
        }
        catch (TargetInvocationException ex) when (ex.InnerException != null)
        {
            throw ex.InnerException.Capture();
        }
    }
Jürgen Steinblock
fuente