Como relançar InnerException sem perder o rastreamento de pilha em c #?

305

Estou chamando, através da reflexão, de um método que pode causar uma exceção. Como posso passar a exceção para o meu interlocutor sem que o reflexo do wrapper o rodeie?
Estou repetindo a InnerException, mas isso destrói o rastreamento de pilha.
Código de exemplo:

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
fonte
1
Existe outra maneira de fazer isso que não requer nenhum vodu. Dê uma olhada na resposta aqui: stackoverflow.com/questions/15668334/…
Timothy Shields
A exceção lançada no método chamado dinamicamente é a exceção interna da exceção "Exceção lançada pelo destino de uma invocação". Ele possui seu próprio rastreamento de pilha. Realmente não há muito o que se preocupar.
ajeh

Respostas:

481

No .NET 4.5 , agora existe a ExceptionDispatchInfoclasse.

Isso permite capturar uma exceção e lançá-la novamente sem alterar o rastreamento de pilha:

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

Isso funciona em qualquer exceção, não apenas AggregateException.

Foi introduzido devido ao awaitrecurso de linguagem C #, que desvenda as exceções internas das AggregateExceptioninstâncias para tornar os recursos de linguagem assíncrona mais parecidos com os recursos de linguagem síncrona.

Paul Turner
fonte
11
Bom candidato para um método de extensão Exception.Rethrow ()?
Nmarler 7/04
8
Observe que a classe ExceptionDispatchInfo está no espaço para nome System.Runtime.ExceptionServices e não está disponível antes do .NET 4.5.
yoyo 13/05
53
Você pode precisar colocar um regular throw;após a linha .Throw (), porque o compilador não saberá que .Throw () sempre lança uma exceção. throw;nunca será chamado como resultado, mas pelo menos o compilador não reclamará se o seu método exigir um objeto de retorno ou for uma função assíncrona.
Todd
5
@Taudris Esta pergunta é especificamente sobre como rever novamente a exceção interna, que não pode ser tratada de maneira especial throw;. Se você usar throw ex.InnerException;o rastreamento de pilha, será reinicializado no ponto em que será novamente reproduzido.
Paul Turner
5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran
86

Ele é possível preservar o rastreamento de pilha antes rethrowing sem reflexão:

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
}

Isso desperdiça muitos ciclos em comparação com as chamadas InternalPreserveStackTracevia delegado em cache, mas tem a vantagem de depender apenas da funcionalidade pública. Aqui estão alguns padrões de uso comuns para funções de preservação de rastreamento de pilha:

// 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
fonte
Parece legal, o que precisa acontecer depois de executar essas funções?
vdboor
2
Na verdade, não é muito mais lento do que invocar InternalPreserveStackTrace(cerca de 6% mais lento com 10.000 iterações). Acessando campos diretamente pela reflexão é cerca de 2,5% mais rápido do que invocandoInternalPreserveStackTrace
Thomas Levesque
1
Eu recomendaria usar o e.Datadicionário com uma string ou uma chave de objeto exclusiva ( static readonly object myExceptionDataKey = new object ()mas não faça isso se precisar serializar exceções em qualquer lugar). Evite modificar e.Message, porque você pode ter código em algum lugar que analise e.Message. A análise e.Messageé ruim, mas pode não haver outra opção, por exemplo, se você precisar usar uma biblioteca de terceiros com práticas inadequadas de exceção.
Anton Tykhyy
10
DoFixups pausas para exceções personalizadas, se eles não têm o ctor serialização
ruslander
3
A solução sugerida não funcionará se a exceção não tiver um construtor de serialização. Sugiro usar a solução proposta em stackoverflow.com/a/4557183/209727 que funcione bem em qualquer caso. Para o .NET 4.5, considere usar a classe ExceptionDispatchInfo.
Davide Icardi 27/01
33

Eu acho que sua melhor aposta seria colocar isso no seu bloco de captura:

throw;

E depois extraia a inerexceção mais tarde.

GEOCHET
fonte
21
Ou remova a tentativa / captura completamente.
21118 Daniel Earwicker
6
@Earwicker. Remover a tentativa / captura não é uma boa solução em geral, pois ignora os casos em que o código de limpeza é necessário antes da propagação da exceção na pilha de chamadas.
314 Jordan Jordan
12
@Jordan - Limpar código se deve estar em um não, finalmente bloquear um bloco catch
Paolo
17
@ Paolo - Se é para ser executado em todos os casos, sim. Se ele deve ser executado apenas em caso de falha, não.
Chiccodoro 01/09/10
4
Lembre-se de que InternalPreserveStackTrace não é um thread seguro, portanto, se você tiver dois threads nesses estados de exceção ... que Deus tenha piedade de todos nós.
Rob
14

Ninguém explicou a diferença entre ExceptionDispatchInfo.Capture( ex ).Throw()e uma planície throw, então aqui está.

A maneira completa de reconfigurar uma exceção capturada é usar ExceptionDispatchInfo.Capture( ex ).Throw()(disponível apenas no .Net 4.5).

Abaixo há os casos necessários para testar isso:

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

O caso 1 e o caso 2 fornecerão um rastreamento de pilha em que o número da linha do código-fonte do CallingMethodmétodo é o número da throw new Exception( "TEST" )linha.

No entanto, o caso 3 fornecerá um rastreamento de pilha em que o número da linha do código-fonte do CallingMethodmétodo é o número da linha da throwchamada. Isso significa que, se a throw new Exception( "TEST" )linha estiver cercada por outras operações, você não terá idéia de qual número de linha a exceção foi realmente lançada.

O caso 4 é semelhante ao caso 2, porque o número da linha da exceção original é preservado, mas não é uma releitura real, pois altera o tipo da exceção original.

jeuoekdcwzfwccu
fonte
5
Eu sempre pensei que 'throw' não redefiniu o stacktrace (em oposição a 'throw e').
Jesper Matthiesen
@JesperMatthiesen Posso estar enganado, mas ouvi dizer que depende se a exceção foi lançada e capturada no mesmo arquivo. Se for o mesmo arquivo, o rastreamento de pilha será perdido, se for outro arquivo, será preservado.
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 );
    }
}

Chame o método de extensão em sua exceção antes de executá-lo, pois ele preservará o rastreamento de pilha original.

Eric
fonte
Esteja ciente de que, no .Net 4.0, o InternalPreserveStackTrace agora não funciona - veja no Reflector e você verá que o método está completamente vazio!
Samuel Jack
Raspe isso: eu estava olhando para o RC: na versão beta, eles colocaram a implementação novamente!
Samuel Jack
3
sugestão: mude PreserveStackTrace para retornar ex - então, para lançar uma exceção, você pode apenas dizer: throw ex.PreserveStackTrace ();
Simon_Weaver
Por que usar Action<Exception>? Aqui usar método estático
Kiquenet
10

Ainda mais reflexão ...

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

Lembre-se de que isso pode ocorrer a qualquer momento, pois os campos particulares não fazem parte da API. Veja mais discussões sobre o Mono bugzilla .

Skolima
fonte
28
Essa é uma idéia muito, muito ruim, pois depende de detalhes internos não documentados sobre as classes de estrutura.
21118 Daniel Earwicker
1
Acontece que é possível preservar o rastreamento da pilha sem reflexão, veja abaixo.
Anton Tykhyy
1
Chamando o interno InternalPreserveStackTracemétodo seria melhor, já que ele faz a mesma coisa e é menos provável de mudança no futuro ...
Thomas Levesque
1
Na verdade, seria pior, pois InternalPreserveStackTrace não existe no Mono.
Skolima 07/06/10
5
@daniel - bem, é uma idéia muito, muito, muito ruim para jogar; redefinir o stacktrace quando todo desenvolvedor .net for treinado para acreditar que não. também é uma coisa muito, muito, muito ruim se você não conseguir descobrir a fonte de uma NullReferenceException e perder um cliente / pedido porque não consegue encontrá-lo. para mim, que supera 'detalhes não documentados' e definitivamente mono.
Simon_Weaver
10

Primeiro: não perca o TargetInvocationException - são informações valiosas quando você deseja depurar as coisas.
Segundo: Embrulhe o TIE como InnerException em seu próprio tipo de exceção e coloque uma propriedade OriginalException vinculada ao que você precisa (e mantenha intacta a pilha de chamadas inteira).
Terceiro: deixe o TIE sair do seu método.

kokos
fonte
5

Gente, você é legal .. Eu vou ser um necromante em breve.

    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
fonte
1
Boa ideia, mas você nem sempre controla o código que chama .Invoke().
precisa saber é o seguinte
1
E você nem sempre sabe os tipos dos argumentos / resultado no momento da compilação.
Roman Starkov
3

Outro código de exemplo que usa serialização / desserialização de exceção. Não requer que o tipo de exceção real seja serializável. Também usa apenas 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
fonte
não exige que o tipo de exceção real seja serializável?
Kiquenet
3

Com base na resposta de Paul Turners, fiz um método de extensão

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

o return exist nunca foi alcançado, mas a vantagem é que eu posso usar throw ex.Capture()como um liner para que o compilador não apresente not all code paths return a valueerros.

    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
fonte