2008-10-20 12 views
23

En el método IDisposable.Dispose, ¿hay alguna forma de averiguar si se está lanzando una excepción?Interceptar una excepción dentro de IDisposable.Disponer

using (MyWrapper wrapper = new MyWrapper()) 
{ 
    throw new Exception("Bad error."); 
} 

Si se produce una excepción en la declaración using Quiero saber sobre él cuando se elimina el objeto de IDisposable.

+0

¿Por qué su envoltorio para la implementación de IDisposable necesita saber por qué se está desechando? –

+4

Quiero escribir un mensaje de registro y sería bueno saber si el código dentro de la declaración se ejecutó correctamente o si se produjo a mitad de camino debido a una excepción. –

+0

Eche un vistazo a esto desde ayende http://ayende.com/Blog/archive/2007/06/20/Did-you-know-Find-out-if-an-exception-was-thrown.aspx –

Respuesta

13

Sin, no hay manera de hacer esto en el marco .Net, no se puede averiguar la corriente de excepción -que-se-ser-arrojado en una cláusula finally.

Vea esto post on my blog, para una comparación con un patrón similar en Ruby, resalta las lagunas que creo que existen con el patrón IDisposable.

Ayende tiene un truco que le permitirá detect an exception happened, sin embargo, no le dirá qué excepción fue.

+0

Buena captura en el sitio "detectar una excepción" que el truco no funciona con asp.net de confianza media. – TamusJRoyce

1

En lugar del azúcar sintáctico de la sentencia using, ¿por qué no simplemente implementar su propia lógica para esto? Algo así como:

try 
{ 
    MyWrapper wrapper = new MyWrapper(); 

} 
catch (Exception e) 
{ 
    wrapper.CaughtException = true; 
} 
finally 
{ 
    if (wrapper != null) 
    { 
     wrapper.Dispose(); 
    } 
} 
+0

Creo su sugerencia general es buena, pero su implementación específica no detectará una excepción arrojada en el método Dispose(). Necesitará envolver los contenidos del bloque finally (o solo la llamada Dispose()) dentro de su propio intento/atrapar bloque –

+0

@MikeB la clase contenedora sabrá si tuvo una excepción. Si es necesario, puede implementar un try-catch internamente. –

+0

MyWrapper debe crearse una instancia fuera del bloque try/catch o el try/catch debe reescribirse de forma diferente. Si MyWrapper arroja una excepción, tiene una posible NullReferenceException en el bloque catch. –

3

James, Todo wrapper puede hacer es registrar sus propias excepciones. No puede obligar al consumidor de wrapper a registrar sus propias excepciones. Eso no es para lo que IDisposable es. IDisposable está destinado a la liberación semideterminista de recursos para un objeto. Escribir el código correcto IDisposable no es trivial.

De hecho, ni siquiera se requiere que el consumidor de la clase llame al método de eliminación de clases, ni se le requiere usar un bloque de uso, por lo que todo se descompone.

Si lo mira desde el punto de vista de la clase contenedora, ¿por qué debería importar que estuviera presente dentro de un bloque de uso y hubo una excepción? ¿Qué conocimiento trae eso? ¿Es un riesgo de seguridad tener un código de terceros informado de los detalles de las excepciones y del seguimiento de la pila? ¿Qué puede hacer wrapper si hay una división por cero en un cálculo?

La única forma de registrar excepciones, independientemente de IDisposable, es try-catch y luego volver a lanzar en el catch.

try 
{ 
    // code that may cause exceptions. 
} 
catch(Exception ex) 
{ 
    LogExceptionSomewhere(ex); 
    throw; 
} 
finally 
{ 
    // CLR always tries to execute finally blocks 
} 

menciona que va a crear una API externa. Tendría que ajustar cada llamada en el límite público de su API con try-catch para registrar que la excepción provino de su código.

Si está escribiendo una API pública, entonces realmente debería leer Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (Microsoft .NET Development Series) - 2nd Edition .. 1st Edition.


Si bien no abogan por ellos, he visto IDisposable utilizado para otros patrones interesantes:

  1. la semántica de transacción auto-rollback. La clase de transacción revertiría la transacción en Dispose si aún no se ha comprometido.
  2. Bloques de código temporizados para el registro. Durante la creación del objeto, se grabó una marca de tiempo, y en Dispose se calculó TimeSpan y se escribió un evento de registro.

* Estos patrones se pueden lograr con otra capa de direccionamiento indirecto y delegados anónimos fácilmente y sin tener que sobrecargar la semántica IDisposable. La nota importante es que su envoltorio IDisposable es inútil si usted o un miembro del equipo se olvida de usarlo correctamente.

+0

La transacción de reversión automática es la razón principal por la que estoy interesado en esto. Esencialmente, así puedo tener un bloque de uso y no tener que comit explícitamente al final, pero aún así permitir que aborte con excepción. No estoy seguro si es mejor ser explícito o no. MS va con explícito para System.Transactions, sin embargo, creo que es posible que los desarrolladores olviden el comit. – tjmoore

+0

Bueno, @tjmoore, notarás que dije Auto-Rollback, no Auto-Commit. Por lo tanto, en este escenario, el método Dispose() de las envolturas se retrotraería si no se comprometiera explícitamente. Sin embargo, no defiendo este patrón ya que es una bastardización de IDisposable. Con la capacidad de pasar delegados anónimos como parámetros a los métodos, podría introducir fácilmente otra capa de indirección para lograr los mismos resultados de una manera mucho más apropiada. –

0

Esto cogerá las excepciones producidas ya sea directamente o dentro del método dispose:

try 
{ 
    using (MyWrapper wrapper = new MyWrapper()) 
    { 
     throw new MyException("Bad error."); 
    } 
} 
catch (MyException myex) { 
    //deal with your exception 
} 
catch (Exception ex) { 
    //any other exception thrown by either 
    //MyWrapper..ctor() or MyWrapper.Dispose() 
} 

pero esto es confiar en ellos el uso de este este código - que suena como desea mywrapper hacerlo en su lugar.

La sentencia using es solo para asegurarse de que se llama a Dispose siempre. Es realmente haciendo esto:

MyWrapper wrapper; 
try 
{ 
    wrapper = new MyWrapper(); 
} 
finally { 
    if(wrapper != null) 
     wrapper.Dispose(); 
} 

Parece que lo que quiere es:

MyWrapper wrapper; 
try 
{ 
    wrapper = new MyWrapper(); 
} 
finally { 
    try{ 
     if(wrapper != null) 
      wrapper.Dispose(); 
    } 
    catch { 
     //only errors thrown by disposal 
    } 
} 

Yo sugeriría hacer frente a esto en su aplicación de botar - usted debe manejar cualquier problema durante la eliminación de todas formas.

Si está bloqueando algún recurso donde necesita que los usuarios de su API lo liberen de alguna manera, considere tener un método Close(). También debe llamarlo (si no lo ha hecho), pero los usuarios de su API también podrían llamarlo ellos mismos si necesitaran un control más preciso.

1

No solo es posible saber si se produjo una excepción cuando se desecha un objeto desechable, sino que incluso se puede obtener la excepción que se arrojó dentro de la cláusula finally con un poco de magia. Mi biblioteca de seguimiento de la herramienta ApiChange emplea este método para rastrear excepciones dentro de una instrucción using. Se puede encontrar más información sobre cómo funciona esto here.

Suyo, Alois Kraus

+0

Esto distinguirá los casos donde (1) la excepción es capturada y manejada (no retroproyectada) dentro de la declaración 'using', (2) una excepción no está atrapada dentro de la instrucción using, y se usa' Dispose' mientras desenrolla la pila o (3) la excepción que no está atrapada dentro de la instrucción using, se captura en primer lugar fuera de ella, pero mientras se desenrolla la pila se produce otra excepción que se captura y maneja dentro de la instrucción using, por lo que la ejecución después del 'Dispose' sigue la ruta "normal". Realmente desearía que 'using' admitiría una derivada 'IDisposable' que utilizara un parámetro ... – supercat

+0

... para indicar si se estaba usando en un contexto" normal "o" excepcional ", ya que no creo que haya otra cosa cualquier forma semánticamente consistente de contar dentro de un procedimiento 'Dispose'. Un gran caso donde esa construcción ayudaría sería con construcciones de bloqueo/mutex. Si una excepción deja un recurso mutex-guardado en mal estado, los intentos de utilizar el recurso no deberían permitirse, pero tampoco deberían bloquear (o seguir bloqueando) para siempre. En cambio, deberían arrojar una excepción. Sería bueno si uno pudiera implementar eso a través de un guardia 'que usa' que podría distinguir la salida normal contra la excepción. – supercat

+0

No estoy instalando un filtro de excepción sino simplemente comprobando si existe una estructura de puntero de excepción que indica un escenario de desenrollado de la pila en la llamada a la disposición. Este es el escenario 2. El escenario 1 también está cubierto, por supuesto, ya que tiene una decisión allí si tiene una pila de desenrollar o no. También es posible hacer 3, pero eso podría ser costoso por lo que aún no lo he intentado. –

0

Si desea permanecer puramente interna en .NET, dos enfoques que sugeriría estarían escribiendo un "try-catch-finally" envoltorio, que aceptaría los delegados de las diferentes partes, o bien escribir un contenedor "usar estilo", que acepte un método para invocar, junto con uno o más objetos identificables ID que se deben eliminar después de que se complete.

Una envoltura de "estilo de uso" podría manejar la eliminación en un bloque try-catch y, si se arroja alguna excepción, envolverlos en una CleanupFailureException que contendría los fallos de eliminación así como cualquier excepción que ocurriera en el delegado principal, o bien agregue algo a la propiedad de "Datos" de la excepción con la excepción original. Yo preferiría incluir cosas en una CleanupFailureException, ya que una excepción que ocurre durante la limpieza generalmente indicará un problema mucho mayor que uno que ocurre en el procesamiento de la línea principal; Además, se podría escribir una CleanupFailureException para incluir múltiples excepciones anidadas (si hay 'n' objetos IDsposable, podría haber n + 1 excepciones anidadas: una de la línea principal y una de cada Dispose).

Un contenedor "try-catch-finally" escrito en vb.net, aunque puede llamarse desde C#, podría incluir algunas características que de otro modo no estarían disponibles en C#, incluida la capacidad de expandirlo a un bloque "try-filter-catch-fault-finally-finally", donde el código "filter" se ejecutaría antes que el la pila se desenrolla de una excepción y determina si la excepción debe capturarse, el bloque "fault" contendría código que solo se ejecutaría si ocurriera una excepción, pero que en realidad no lo capturaría, y tanto el bloque "fault" como el "finally" recibiría parámetros que indicaran qué excepción (si alguna) ocurrió durante la ejecución del "try", y si el "try" se completó exitosamente (nota, por cierto, que sería posible que el parámetro de excepción no sea nulo incluso si la línea principal se completa; el código C# puro no pudo detectar tal condición, pero el contenedor vb.net podría).

2

Puede hacer esto mediante la implementación del método Dispose para la clase "MyWrapper". En el método dispose puede comprobar para ver si hay una excepción de la siguiente manera

public void Dispose() 
{ 
    bool ExceptionOccurred = Marshal.GetExceptionPointers() != IntPtr.Zero 
          || Marshal.GetExceptionCode() != 0; 
    if(ExceptionOccurred) 
    { 
     System.Diagnostics.Debug.WriteLine("We had an exception"); 
    } 
} 
5

No es posible para capturar la excepción en el método Dispose().

Sin embargo, es posible marcar Marshal.GetExceptionCode() en el Dispose para detectar si se produjo una excepción, pero no confiaría en eso.

Si usted no necesita una clase y desea que desea es capturar la excepción, puede crear una función que acepta un lambda que se ejecuta dentro de un bloque try/catch, algo como esto:

HandleException(() => { 
    throw new Exception("Bad error."); 
}); 

public static void HandleException(Action code) 
{ 
    try 
    { 
     if (code != null) 
      code.Invoke(); 
    } 
    catch 
    { 
     Console.WriteLine("Error handling"); 
     throw; 
    } 
} 

Como ejemplo, puede usar un método que realice automáticamente un Commit() o Rollback() de una transacción y realice algún registro. De esta manera, no siempre necesita un bloque de prueba/captura.

public static int? GetFerrariId() 
{ 
    using (var connection = new SqlConnection("...")) 
    { 
     connection.Open(); 
     using (var transaction = connection.BeginTransaction()) 
     { 
      return HandleTranaction(transaction,() => 
      { 
       using (var command = connection.CreateCommand()) 
       { 
        command.Transaction = transaction; 
        command.CommandText = "SELECT CarID FROM Cars WHERE Brand = 'Ferrari'"; 
        return (int?)command.ExecuteScalar(); 
       } 
      }); 
     } 
    } 
} 

public static T HandleTranaction<T>(IDbTransaction transaction, Func<T> code) 
{ 
    try 
    { 
     var result = code != null ? code.Invoke() : default(T); 
     transaction.Commit(); 
     return result; 
    } 
    catch 
    { 
     transaction.Rollback(); 
     throw; 
    } 
} 
+1

Deseo que 'Dispose' haya sido definido para tomar un parámetro de tipo' Exception' que indique qué, si hay alguna, excepción estaba pendiente para el contexto try/finally (si hay alguno) * guarding the object *. Indagando si hay un La excepción pendiente para el hilo no es exactamente lo mismo, ya que un 'Dispose' podría invocarse en un bloque try/finally que está anidado dentro del que protege el objeto que se va a eliminar. – supercat

13

Puede extender IDisposable con el método Complete y patrón de uso de esa manera:

using (MyWrapper wrapper = new MyWrapper()) 
{ 
    throw new Exception("Bad error."); 
    wrapper.Complete(); 
} 

Si se produce una excepción dentro de la declaración usingComplete no va a ser llamada antes de Dispose.

Si desea saber qué excepción exacta se produce, suscríbase en el evento AppDomain.CurrentDomain.FirstChanceException y almacene la última excepción lanzada en la variable ThreadLocal<Exception>.

Tal patrón implementado en la clase TransactionScope.

0

En mi caso, quería hacer esto para iniciar sesión cuando un microservicio falla. Ya tengo un using listo para limpiar correctamente antes de que se cierre una instancia, pero si es por una excepción quiero ver por qué, y no me gusta la respuesta.

En lugar de tratar de hacer que funcione en Dispose(), tal vez haga un delegado para el trabajo que necesita hacer, y luego ajuste su captura de excepciones allí. Así que en mi registrador mywrapper, agrego un método que toma una acción/Func:

public void Start(Action<string, string, string> behavior) 
    try{ 
     var string1 = "my queue message"; 
     var string2 = "some string message"; 
     var string3 = "some other string yet;" 
     behaviour(string1, string2, string3); 
    } 
    catch(Exception e){ 
     Console.WriteLine(string.Format("Oops: {0}", e.Message)) 
    } 
} 

de implementar:

using (var wrapper = new MyWrapper()) 
    { 
     wrapper.Start((string1, string2, string3) => 
     { 
      Console.WriteLine(string1); 
      Console.WriteLine(string2); 
      Console.WriteLine(string3); 
     } 
    } 

Dependiendo de lo que tiene que hacer, esto puede ser demasiado restrictiva, pero trabajé para lo que necesitaba.

Cuestiones relacionadas