2010-05-13 13 views
16

Mam następujący kod w mojej aplikacji:Wykrywanie Dispose() z wyjątkiem wewnątrz przy bloku

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

Ten kod powoduje utylizować metodą jak zwykle klasa Database(), a metoda ta automatycznie zatwierdza transakcję do bazy danych, ale to pozostawia moją bazę danych w niespójnym stanie, ponieważ odpowiedzi są usuwane, ale pytanie i ankieta nie są.

Jest jakiś sposób, który mogę wykryć w metodzie Dispose(), że jest wywoływany z powodu wyjątku zamiast zwykłego końca bloku zamykającego, więc mogę zautomatyzować wycofywanie zmian?

Nie chcę ręcznie dodawać bloku try ... catch, moim celem jest użycie bloku using jako logicznego bezpiecznego menedżera transakcji, więc zobowiązuje się do bazy danych, jeśli wykonanie było czyste lub wycofanie, jeśli jakiekolwiek wystąpił wyjątek.

Czy masz jakieś przemyślenia na ten temat?

Odpowiedz

23

Jak już powiedzieli inni, użycie tego wzoru jednorazowego w tym celu jest przyczyną problemów. Jeśli wzór działa przeciwko tobie, to powinienem zmienić wzór.Dokonując zatwierdzenia domyślnego zachowania bloku używającego, zakłada się, że każde użycie bazy danych powoduje zatwierdzenie, co oczywiście nie ma miejsca - szczególnie jeśli wystąpi błąd. Wyraźne zatwierdzenie, ewentualnie połączone z blokiem try/catch, działałoby lepiej.

Jednak jeśli naprawdę chcesz, aby utrzymać stosowanie wzorca jak, można użyć:

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

w Displose realizacji celu ustalenia, czy wyjątek został rzucony (więcej szczegółów here) .

+7

Adrian, masz to, nie chcę trenować o użyciu wzoru, tylko rozwiązanie techniczne, działało pięknie. –

+0

Proszę spojrzeć na moją odpowiedź. Próbuję wyjaśnić, dlaczego sprawdzanie wyjątków w metodzie "Dispose" jest złym pomysłem. – Steven

+1

@Steven: Jak większość odpowiedzi (w tym Twoja) zgodziła się, nie jest to mądry wzorzec do użycia. Mimo to pierwotne pytanie spytało, czy * możliwe * jest wiedzieć, czy wyjątek został zgłoszony w metodzie "Dispose", co pokazuje moja odpowiedź - nawet jeśli ma ona powiązane problemy. – adrianbanks

0

Można dziedziczyć z klasy Database, a następnie przesłonić metodę Dispose() (upewnij się, że zamknąłeś zasoby db), może to spowodować niestandardowe zdarzenie, do którego możesz się zapisać w swoim kodzie.

+0

Dlaczego utylizować być wirtualny? –

1

Należy owinąć zawartość Twojego wykorzystaniem bloku w try/catch i wycofania się z transakcji w bloku 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

Ja nie chcę, chcę użyć "używania" jako pomocnika dla tej operacji, z uzależnieniem od innych elementów, które muszę wykonać. –

+0

@Augusto - wycofanie działa dobrze przy "użyciu". Zobacz moją zaktualizowaną odpowiedź –

+3

Używanie to po prostu skrócona składnia dla try {} finally {o.Dispose()} Zastępowanie użycia pojedynczą próbą {} catch {rollback} finally {dispose} jest poprawną rzeczą do zrobienia. – chilltemp

7

Spójrz na projektowaniu dla TransactionScope w System.Transactions. Ich metoda wymaga wywołania metody Complete() w zakresie transakcji, aby zatwierdzić transakcję. uważam projektowania klasy bazy danych, aby śledzić ten sam wzór:

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

Możesz chcieć wprowadzić pojęcie transakcji spoza obiektu Database chociaż. Co się stanie, jeśli konsumenci będą chcieli korzystać z Twojej klasy i nie będą chcieli korzystać z transakcji i czy wszystko będzie automatycznie zatwierdzane?

1

Jak podkreśla Anthony, problemem jest błąd w używaniu klauzuli "use" w tym scenariuszu. Paradygmat IDisposable ma na celu upewnienie się, że zasoby obiektu są czyszczone niezależnie od wyniku scenariusza (stąd wyjątek, powrót lub inne zdarzenie, które opuszcza blok używający, nadal wyzwala metodę Dispose). Ale przekartkowałeś to, by oznaczać coś innego, aby dokonać transakcji.

Moja sugestia byłaby taka, jak stwierdzili inni i używają tego samego paradygmatu co TransactionScope. Deweloper powinien wyraźnie wywołać metodę Commit lub podobną na końcu transakcji (przed zamknięciem bloku przy użyciu), aby wyraźnie stwierdzić, że transakcja jest dobra i gotowa do zatwierdzenia. Tak więc, jeśli wyjątek powoduje, że wykonanie pozostawia blok przy użyciu, metoda Dispose w tym przypadku może zamiast tego wykonać Rollback. To nadal pasuje do paradygmatu, ponieważ wykonanie Rollback byłoby sposobem na "wyczyszczenie" obiektu Database, aby nie pozostało ono niepoprawne.

Ta zmiana w projekcie znacznie ułatwiłaby robienie tego, co chcesz zrobić, ponieważ nie będziesz walczył z projektem .NET, aby spróbować "wykryć" wyjątek.

9

Wszyscy inni już napisali, jaki powinien być prawidłowy projekt twojej klasy , więc tego nie powtórzę. Jednak nie widziałem żadnego wyjaśnienia, dlaczego to, co chcesz, nie jest możliwe. Więc oto idzie.

To, co chcesz zrobić, to wykryć podczas wywołania Dispose, że ta metoda jest wywoływana w kontekście wyjątku. Gdy będziesz w stanie to zrobić, programiści nie będą musieli jawnie dzwonić pod numer Commit. Jednak problem polega na tym, że nie ma możliwości niezawodnego wykrycia tego w .NET. Podczas gdy istnieją mechanizmy sprawdzające ostatni zgłoszony błąd (np. HttpServerUtility.GetLastError), mechanizmy te są specyficzne dla hosta (w związku z tym ASP.NET ma inny mechanizm, na przykład jako formularze systemu Windows). I chociaż można napisać implementację dla konkretnej implementacji hosta, na przykład implementację, która działałaby tylko w środowisku ASP.NET, jest jeszcze jeden ważniejszy problem: co, jeśli twoja klasa Database jest używana lub tworzona w kontekście wyjątku ? Oto przykład:

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

Gdy klasa Database jest używany w kontekście Exception, jak w powyższym przykładzie, jaki jest Twój sposób Dispose wiedzieć, że nadal musi popełnić? Mogę wymyślić sposoby obejścia tego, ale będzie to dość kruche i podatne na błędy. Dać przykład.

Podczas tworzenia pliku Database można sprawdzić, czy jest on wywoływany w kontekście wyjątku, a jeśli tak jest, zapisz ten wyjątek. W czasie wywołania Dispose sprawdza się, czy ostatni zgłoszony wyjątek różni się od wyjątku zapisanego w pamięci podręcznej. Jeśli to się zmieni, powinieneś wycofać. Jeśli nie, zatwierdz.

Chociaż wydaje się to dobrym rozwiązaniem, co z tym przykładem kodu?

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

W tym przykładzie widać, że instancja Database jest tworzony przed blokiem try. Dlatego nie może poprawnie wykryć, że nie może wycofać. Chociaż może to być przykładowy przykład, pokazuje trudności, które napotkasz, próbując zaprojektować swoją klasę w sposób, w jaki nie jest potrzebne żadne wyraźne wywołanie do Commit.

W końcu utworzysz klasę Database trudną do zaprojektowania, trudną w utrzymaniu i nigdy nie naprawisz.

jak wszyscy inni już wspomniano, projekt, który wymaga wyraźnej Commit lub Complete połączenia, byłoby łatwiejsze do wdrożenia, łatwiej uzyskać prawo, łatwiejsze w utrzymaniu i daje kod użytkowania, który jest bardziej czytelny (na przykład dlatego, że wygląda na to, czego oczekują deweloperzy).

Ostatnia uwaga, jeśli martwisz się o deweloperzy zapominając wywołać tę metodę Commit: można zrobić kilka kontroli w metodzie Dispose aby sprawdzić, czy jest ona wywołana bez Commit nazywa i pisać do konsoli lub ustawić punkt przerwania podczas debugowanie. Kodowanie takiego rozwiązania byłoby jeszcze łatwiejsze niż próba pozbycia się w ogóle.

Aktualizacja: Adrian wrote interesującą alternatywą dla korzystania HttpServerUtility.GetLastError. Jak zauważa Adrian, możesz użyć Marshal.GetExceptionPointers(), który jest ogólnym sposobem, który działa na większości hostów. Należy zauważyć, że to rozwiązanie ma te same wady, które wyjaśniono powyżej i że wywoływanie klasy Marshal jest możliwe tylko w pełnym zaufaniu.

+0

Jak można wykryć wyjątek w wywołaniu funkcji '.Dispose()'? –

+0

@Lasse: Jak można przeczytać w mojej odpowiedzi, jedną z możliwości jest wywołanie HttpServerUtility.GetLastError, gdy twój kod działa na hoście ASP.NET. – Steven

+1

+1 dla "Co zrobić, jeśli chcesz pisać do bazy danych w przypadku wyjątku" – cjk

2

W skrócie: myślę, że to niemożliwe, ale

Co można zrobić, to ustawić flagę na swojej klasie baza danych z Domyślna wartość „false” (to nie jest dobry, aby przejść) i na ostatnim wierszu wewnątrz przy blokujesz wywołanie metody, która ustawia ją na "true", a następnie w metodzie Dispose() możesz sprawdzić, czy flaga "ma wyjątek" czy nie.

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

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

I klasa bazie

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

Niestety nie jest to właściwy projekt klasy. "Usuń" powinno robić tak mało, jak to tylko możliwe. Kiedy robisz coś więcej niż tylko wyrzucanie zasobów w metodzie "Dispose", zmiany wyjątku wyrzucanego z tej metody "Dispose" są wyższe. 'Dispose' jest również wywoływany w kontekście' Exception', iw takim przypadku zastępowałbyś ten wyjątek nowym, powodując, że debugowanie i znajdowanie przyczyny wyjątku jest trudniejsze. Ponadto nie należy aktywnie wycofywać transakcji db w "Dispose" z powodu tej samej zmiany. Po prostu zlikwiduj transakcję bazową. – Steven

+0

Myślę, że ten komentarz powinien być na pytanie, a nie na moją odpowiedź, ale tak czy inaczej ... "Dispose jest również nazywany w kontekście wyjątku" on wie, i dlatego chce dowiedzieć się, czy istnieje lub nie jest wyjątkiem i zamiast rzucać wyjątek, powinien spróbować złapać CommitToDatabase i Rollback lub coś w tym stylu, – BrunoLM

+0

Skomentowałem kod, który widziałem w Twojej odpowiedzi, ale masz rację: Mój komentarz byłby naprawdę bardziej odpowiednie w odniesieniu do samego pytania. btw. Bardzo trudno jest uzyskać odpowiedni projekt, próbując to zrobić bez jawnej metody "Commit", jak starałem się wyjaśnić w mojej odpowiedzi: http://stackoverflow.com/questions/2830073/detecting-a-dispose-from -an-exception-inside-using-block/2830501 # 2830501 – Steven

Powiązane problemy