2010-05-13 10 views
16

tengo el siguiente código en mi aplicación:Detección de una Dispose() de una excepción en el interior utilizando el bloque

using (var database = new Database()) { 
    var poll = // Some database query code. 

    foreach (Question question in poll.Questions) { 
     foreach (Answer answer in question.Answers) { 
      database.Remove(answer); 
     } 

     // This is a sample line that simulate an error. 
     throw new Exception("deu pau"); 

     database.Remove(question); 
    } 

    database.Remove(poll); 
} 

Este código activa el método de la clase de base de datos Dispose(), como de costumbre, y este método confirma automáticamente la transacción a la base de datos, pero esto deja mi base de datos en un estado inconsistente ya que las respuestas se borran pero la pregunta y la encuesta no lo son.

¿Hay alguna manera que pueda detectar en el método Dispose() que se llama debido a una excepción en lugar del final regular del bloque de cierre, para que pueda automatizar la reversión?

No quiero agregar manualmente un bloque try ... catch, mi objetivo es usar el bloque using como un administrador lógico de transacciones seguras, por lo que se compromete con la base de datos si la ejecución fue limpia o se revierte si hay excepción ocurrida.

¿Tiene alguna idea al respecto?

Respuesta

23

Como han dicho otros, su uso del patrón Desechable para este propósito es lo que le está causando los problemas. Si el patrón funciona en su contra, entonces cambiaría el patrón.Al realizar un commit del comportamiento predeterminado del bloque using, usted está asumiendo que cada uso de una base de datos da como resultado una confirmación, lo que claramente no es el caso, especialmente si ocurre un error. Una confirmación explícita, posiblemente combinada con un bloque try/catch, funcionaría mejor.

Sin embargo, si realmente desea mantener su uso del patrón que es, puede utilizar:

bool isInException = Marshal.GetExceptionPointers() != IntPtr.Zero 
         || Marshal.GetExceptionCode() != 0; 

en su aplicación Displose para determinar si una excepción ha sido lanzado (más detalles here) .

+7

Adrián, lo tienes, no quiero que te enseñen sobre los usos de patrones, solo una solución técnica, funcionó maravillosamente. –

+0

Por favor, eche un vistazo a mi respuesta. Intento explicar por qué es una mala idea buscar excepciones dentro de un método 'Dispose'. – Steven

+1

@Steven: como la mayoría de las respuestas (incluida la suya) han acordado, este no es un patrón inteligente para usar. Aun así, la pregunta original preguntó si es * posible * saber si se ha lanzado una excepción cuando estoy en un método 'Dispose', que es lo que muestra mi respuesta, incluso si tiene problemas asociados. – adrianbanks

0

Puede heredar de la clase Database y luego anular el método Dispose() (cerciorándose de cerrar los recursos db), esto podría generar un evento personalizado al que pueda suscribirse en su código.

+0

Por qué se desharía ser virtual? –

1

Usted debe envolver el contenido de su uso de bloque en un try/catch y deshacer la transacción en el bloque catch:

using (var database = new Database()) try 
{ 
    var poll = // Some database query code. 

    foreach (Question question in poll.Questions) { 
     foreach (Answer answer in question.Answers) { 
      database.Remove(answer); 
     } 

     // This is a sample line that simulate an error. 
     throw new Exception("deu pau"); 

     database.Remove(question); 
    } 

    database.Remove(poll); 
} 
catch(/*...Expected exception type here */) 
{ 
    database.Rollback(); 
} 
+0

No quiero, quiero usar "usar" como ayudante para esa operación, con adicción a otra repetición que tengo que hacer. –

+0

@Augusto - rollback funciona bien con "usar". Ver mi respuesta actualizada –

+3

Usar es simplemente una sintaxis de acceso directo para try {} finally {o.Dispose()} Reemplazar el uso con una sola prueba {} catch {rollback} finally {disponer} es lo correcto. – chilltemp

7

mirada en el diseño para TransactionScope en System.Transactions. Su método requiere que llame a Complete() en el alcance de la transacción para confirmar la transacción. Me volvería a considerar el diseño de su clase de base de datos para seguir el mismo patrón:

using (var db = new Database()) 
{ 
    ... // Do some work 
    db.Commit(); 
} 

Es posible que desee introducir el concepto de transacciones fuera de su base de datos de objetos sin embargo. ¿Qué sucede si los consumidores quieren usar su clase y no quieren usar transacciones y tienen todo confirmado automáticamente?

1

Como Anthony señala anteriormente, el problema es un error en el uso de la cláusula using en este escenario. El paradigma IDisposable está destinado a garantizar que los recursos de un objeto se limpien independientemente del resultado de un escenario (por lo tanto, una excepción, retorno u otro evento que abandone el bloque de uso sigue activando el método Dispose). Pero lo ha reutilizado para que signifique algo diferente, para comprometer la transacción.

Mi sugerencia sería como otros han declarado y utilizan el mismo paradigma que TransactionScope. El desarrollador debe tener que invocar explícitamente un Commit o método similar al final de la transacción (antes de que se cierre el bloque de uso) para indicar explícitamente que la transacción es buena y está lista para comprometerse. Por lo tanto, si una excepción hace que la ejecución abandone el bloque que usa, el método Dispose en este caso puede hacer un Rollback. Esto todavía encaja en el paradigma, ya que hacer una reversión sería una forma de "limpiar" el objeto de la base de datos para que no quede un estado inválido.

Este cambio en el diseño también haría mucho más fácil hacer lo que desea hacer, ya que no luchará contra el diseño de .NET para tratar de "detectar" una excepción.

9

Todos los demás ya han escrito cuál es el diseño correcto de su clase Database, por lo que no lo repetiré. Sin embargo, no vi ninguna explicación de por qué lo que quieres no es posible. Entonces aquí va.

Lo que quiere hacer es detectar, durante la llamada al Dispose, que se llama a este método en el contexto de una excepción. Cuando pueda hacer esto, los desarrolladores no tendrán que llamar al Commit explícitamente. Sin embargo, el problema aquí es que no hay manera de detectar esto de manera confiable en .NET. Si bien existen mecanismos para consultar el último error lanzado (como HttpServerUtility.GetLastError), estos mecanismos son específicos del host (por lo que ASP.NET tiene otro mecanismo como formularios de Windows, por ejemplo). Y aunque podría escribir una implementación para una implementación de host específica, por ejemplo, una implementación que solo funcionaría en ASP.NET, hay otro problema más importante: ¿qué ocurre si se usa su clase Database o se crea dentro del contexto de una excepción? ? Aquí se muestra un ejemplo:

try 
{ 
    // do something that might fail 
} 
catch (Exception ex) 
{ 
    using (var database = new Database()) 
    { 
     // Log the exception to the database 
     database.Add(ex); 
    } 
} 

Cuando se utiliza la clase Database en el contexto de un Exception, como en el ejemplo anterior, cómo se supone que su método de Dispose saber que todavía debe comprometerse? Puedo pensar en formas de evitar esto, pero será bastante frágil y propenso a errores. Para dar un ejemplo.

Durante la creación del Database, puede comprobar si se llama en el contexto de una excepción, y si ese es el caso, almacene esa excepción. Durante el tiempo en que se llama al Dispose, comprueba si la última excepción lanzada difiere de la excepción en caché. Si difiere, deberías retroceder. Si no, cometer.

Si bien esto parece una buena solución, ¿qué pasa con este ejemplo de código?

var logger = new Database(); 
try 
{ 
    // do something that might fail 
} 
catch (Exception ex) 
{ 
    logger.Add(ex); 
    logger.Dispose(); 
} 

En el ejemplo se ve que un ejemplo Database se crea antes de que el bloque try. Por lo tanto, no puede detectar correctamente que no podría retroceder. Si bien este podría ser un ejemplo artificial, muestra las dificultades que tendrá que enfrentar al tratar de diseñar su clase de una manera que no sea necesaria una llamada explícita a Commit.

Al final harás que tu clase Database sea difícil de diseñar, difícil de mantener y nunca lo harás bien.

Como todos los demás ya se ha dicho, un diseño que necesita una explícita Commit o Complete llamada, sería más fácil de implementar, fácil de hacerlo bien, más fácil de mantener, y da código de uso que es más fácil de leer (por ejemplo, porque mira lo que los desarrolladores esperan).

última nota, si usted está preocupado por los desarrolladores de olvidarse de llamar a este método Commit: usted puede hacer algunas comprobaciones en el método Dispose para ver si se le llama sin Commit se llama y escribir en la consola o un punto de ruptura, mientras depuración Codificar una solución así sería mucho más fácil que tratar de deshacerse del Commit en absoluto.

Actualización: Adrian wrote una alternativa intersting a usar HttpServerUtility.GetLastError. Como señala Adrian, puede usar Marshal.GetExceptionPointers(), que es una forma genérica que funcionaría en la mayoría de los hosts. Tenga en cuenta que esta solución tiene los mismos inconvenientes explicados anteriormente y que llamar a la clase Marshal solo es posible en plena confianza

+0

¿Cómo se detecta la excepción en una llamada a '.Dispose()'? –

+0

@Lasse: como puede leer en mi respuesta, una posibilidad es llamar a HttpServerUtility.GetLastError cuando su código se ejecuta en el host ASP.NET. – Steven

+1

+1 para "¿Qué sucede si desea escribir en la base de datos en el caso de una excepción?" – cjk

2

En resumen: creo que eso es imposible, pero

Lo que puede hacer es fijar una bandera en su clase de base de datos con valor por defecto "false" (no es bueno para ir) y en la última línea en el interior utilizando bloquee usted llama a un método que lo establece en "verdadero", luego en el método Dispose() puede verificar si el indicador "tiene excepción" o no.

using (var db = new Database()) 
{ 
    // Do stuff 

    db.Commit(); // Just set the flag to "true" (it's good to go) 
} 

Y la clase de base de datos

public class Database 
{ 
    // Your stuff 

    private bool clean = false; 

    public void Commit() 
    { 
     this.clean = true; 
    } 

    public void Dispose() 
    { 
     if (this.clean == true) 
      CommitToDatabase(); 
     else 
      Rollback(); 
    } 
} 
+0

Desafortunadamente este no es un diseño apropiado de una clase. 'Dispose' debe hacer lo menos posible. Cuando está haciendo algo más que eliminar recursos en un método 'Dispose', los cambios de una excepción lanzada desde ese método 'Dispose' son más altos. 'Dispose' también se llama en el contexto de una' Exception', y en ese caso reemplazaría esa excepción por una nueva, dificultando la depuración y encontrando la causa de la excepción. Además, no revierte activamente una transacción db en un 'Dispose' debido a la misma reasión. Solo deseche la transacción subyacente. – Steven

+0

Supongo que este comentario debería estar en la pregunta, no en mi respuesta, pero de todos modos ... "Eliminar también se llama en el contexto de una excepción" él lo sabe, y es por eso que quiere saber si hubo o no no es una excepción, y en lugar de arrojar una excepción de nuevo, tendría una captura de prueba alrededor de CommitToDatabase y Rollback o algo así – BrunoLM

+0

Comenté el código que vi en su respuesta, pero tiene razón al respecto: Mi comentario sería de hecho más apropiado en la pregunta en sí. por cierto. Es muy difícil obtener el diseño correcto al intentar hacer esto sin un método 'Commit' explícito, como traté de explicar en mi respuesta: http://stackoverflow.com/questions/2830073/detecting-a-dispose-from -an-exception-inside-using-block/2830501 # 2830501 – Steven

Cuestiones relacionadas