Interceptando una excepción dentro de IDisposable.Dispose

En el método IDisposable.Dispose , ¿hay una manera de averiguar si se está IDisposable.Dispose una excepción?

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

Si se lanza una excepción en la statement de using , quiero saberlo cuando el objeto IDisposable esté dispuesto.

No , no hay manera de hacer esto en el marco de .Net, no puede descubrir la excepción actual que se está lanzando en una cláusula finalmente.

Vea esta publicación en mi blog , para una comparación con un patrón similar en Ruby, resalta las brechas que creo que existen con el patrón IDisposable.

Ayende tiene un truco que te permitirá detectar una excepción ocurrida , sin embargo, no te dirá qué excepción fue.

Puede extender IDisposable con el método Complete y usar un patrón como ese:

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

Si se lanza una excepción dentro de la statement de using Complete , no se llamará antes de Dispose .

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

Tal patrón implementado en la clase TransactionScope .

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

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

Si no necesita una clase y solo desea capturar la Excepción, puede crear una función que acepte 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 necesitas un bloque try / catch.

 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(IDbTransaction transaction, Func code) { try { var result = code != null ? code.Invoke() : default(T); transaction.Commit(); return result; } catch { transaction.Rollback(); throw; } } 

James, todo lo que el wrapper puede hacer es registrar sus propias excepciones. No puede forzar al consumidor de wrapper a registrar sus propias excepciones. Para eso no es idisposible. IDisposable está destinado a la liberación semi-determinista de recursos para un objeto. Escribir código IDisponible correcto no es trivial.

De hecho, el consumidor de la clase ni siquiera tiene que llamar a su método de eliminación de clases, ni tampoco está obligado a usar un bloque de uso, por lo que todo se rompe.

Si lo observa desde el punto de vista de la clase de envoltorio, ¿por qué debería importarle que estuviera presente dentro de un bloque de uso y haya una excepción? ¿Qué conocimiento trae eso? ¿Es un riesgo de seguridad tener un código de terceros que tenga acceso a los detalles de excepción y al seguimiento de la stack? ¿Qué puede hacer una 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 el catch.

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

Usted menciona que está creando una API externa. Tendría que envolver 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 las Pautas de diseño del marco: Convenciones, expresiones idiomáticas y patrones para bibliotecas .NET reutilizables (Microsoft .NET Development Series) – 2ª edición . 1ª edición .


Aunque no los defiendo, he visto el uso de IDisposable para otros patrones interesantes:

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

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

Puede hacer esto comprando implementando el método Dispose para la clase “MyWrapper”. En el método de disposición, puede verificar 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"); } } 

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

 try { MyWrapper wrapper = new MyWrapper(); } catch (Exception e) { wrapper.CaughtException = true; } finally { if (wrapper != null) { wrapper.Dispose(); } } 

No solo es posible descubrir si se lanzó una excepción cuando se desecha un objeto desechable, sino que también puede obtener la excepción que se lanzó dentro de la cláusula finalmente con un poco de magia. Mi biblioteca de Rastreo de la herramienta ApiChange emplea este método para rastrear excepciones dentro de una statement de uso. Más información sobre cómo funciona esto aquí .

Atentamente, Alois Kraus

Esto detectará las excepciones lanzadas directamente o dentro del método de disposición:

 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 depende de que usen este código: parece que quieres que MyWrapper haga eso en su lugar.

La statement de uso es solo para asegurarse de que siempre se llame a Dispose. Realmente está haciendo esto:

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

Suena como lo que quieres es:

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

Le sugeriría que se ocupe de esto en su implementación de Disposición: debería manejar cualquier problema durante la eliminación de todos modos.

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

Si desea permanecer únicamente dentro de .net, sugeriría dos enfoques sería escribir un contenedor “try-catch-finally”, que acepte delegates para las diferentes partes, o bien escribir un contenedor “using-style”, que acepte un método a invocar, junto con uno o más objetos IDisposibles que deben eliminarse una vez que se hayan completado.

Un envoltorio de “estilo de uso” podría manejar la eliminación en un bloque try-catch y, si se eliminan excepciones, envuélvalos en una CleanupFailureException que contenga los fallos de eliminación así como cualquier excepción que haya ocurrido en el delegado principal , o bien agregue algo a la propiedad “Datos” de la excepción con la excepción original. Preferiría envolver las cosas en una excepción CleanupFailureException, ya que una excepción que ocurre durante la limpieza generalmente indicará un problema mucho mayor que el que ocurre en el procesamiento de la línea principal; además, se podría escribir una excepción CleanupFailureException para incluir múltiples excepciones anidadas (si hay ‘n’ objetos IDisponibles, podría haber n + 1 excepciones anidadas: una de la línea principal y una de cada Dispose).

Una envoltura de “try-catch-finally” escrita en vb.net, aunque puede llamarse desde C #, podría incluir algunas funciones que de otra manera no están disponibles en C #, incluida la capacidad de expandirla a un “try-filter-catch-fault-finally” bloque, donde el código del “filtro” se ejecutaría antes de que la stack se desenrollara de una excepción y determine si la excepción se debe capturar, el bloque de “falla” contendría un código que solo se ejecutaría si se produjera una excepción, pero en realidad no se detectaría y los bloques “fallo” y “finalmente” recibirían parámetros que indicaban qué excepción (si hubiera) ocurría durante la ejecución del “bash”, y si el “bash” se completaba con éxito (note, por cierto, que lo haría). es 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).

En mi caso, quería hacer esto para iniciar sesión cuando se bloquea un microservicio. Ya he implementado un using para limpiar correctamente justo antes de que se cierre una instancia, pero si eso se debe a una excepción, quiero ver por qué, y odio el no por respuesta.

En lugar de tratar de hacer que funcione en Dispose() , tal vez haga un delegado para el trabajo que necesita hacer y luego envuelva su captura de excepciones allí. Entonces, en mi registrador MyWrapper, agrego un método que toma un Action / Func:

  public void Start(Action 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)) } } 

Para implementar:

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

Dependiendo de lo que necesite hacer, esto puede ser demasiado restrictivo, pero funcionó para lo que necesitaba.

Ahora, en 2017, esta es la forma genérica de hacerlo, incluido el manejo de la reversión de excepciones.

  public static T WithinTransaction(this IDbConnection cnn, Func fn) { cnn.Open(); using (var transaction = cnn.BeginTransaction()) { try { T res = fn(transaction); transaction.Commit(); return res; } catch (Exception) { transaction.Rollback(); throw; } finally { cnn.Close(); } } } 

y lo llamas así:

  cnn.WithinTransaction( transaction => { var affected = ..sqlcalls..(cnn, ..., transaction); return affected; });