Automatyczne zwracanie z serwisu informacji o błędzie
Zadaniem, które wziąłem na warsztat w poprzednim tygodniu, była refaktoryzacja obsługi optimistic concurrency. Do tej pory obsługa konfliktu wersji przy aktualizacji rekordu była obsługiwana za pomocą wyjątku rzucanego z serwisu i łapanego w filtrze MVC. To oczywiście był błąd projektowy, bo przecież wystąpienie optimistic concurrency to spodziewany efekt edycji rekordu, a nie żaden wyjątek. Poprawiając to, stwierdziłem że dobrze będzie przy okazji w ogóle poprawić komunikację na linii serwis-kontroler, tak aby wyjątki z aplikacji nigdy nie docierały do GUI.
Po co?
Jest to potrzebne chociażby dlatego, że w przyszłości warstwa aplikacyjna może przecież pracować na innej fizycznej maszynie niż warstwa prezentacji, więc rzucenie wyjątku na taką odległość niekoniecznie się uda. Z tego powodu lepiej od razu mieć możliwość przesłania informacji o sukcesie/błędzie operacji w postaci obiektu, no i oczywiście najlepiej by było, jeśli będzie się to działo automatycznie. Poza tym, użytkownikowi warto wyświetlać jakieś lokalizowane komunikaty o błędach. Lokalizowane przez nas, a nie zlokalizowaną wersję .NET Frameworka z jego mądrościami ludowymi w stylu: “Odwołaniem do obiektu nie zostało ustawione na wystąpienie obiektu” albo “Do członka wystąpienia klasy nie można odwołać się z obrębu udostępnionej metody lub udostępnionego inicjatora członka bez jawnego wystąpienia klasy”. ;)
Dobra, dość drwin z Microsoftu, bo nie wygram XBoxa.
Typy do przesyłania odpowiedzi
Do tej pory metody ICrudServiceBase
zwracały obiekt (GetJakiśtamModel), ID obiektu (Create) , DataPageResult<T>
albo niczego nie zwracały (w przypadku operacji Update i Delete). To było złe, więc zacząłem od poprawienia tego.
Najpierw utworzyłem taki enum opisujący możliwe statusy odpowiedzi:
Następnie stworzyłem taką prostą hierarchię typów:
Jak widać - mamy abstrakcyjną wersję bazową, a do tego dwie konkretne - jedną dla metod bez danych wynikowych, drugą mogącą zwrócić wynik typu generycznego. Mogłem oczywiście zrobić tak, żeby generyczna wersja dziedziczyła z niegenerycznej, ale:
- Po pierwsze (o czym wielu programistów nie pamięta) nie dziedziczy się z klas nieabstrakcyjnych (o ile nie jest to konieczne, czyli np. nie hakujemy cudzego frameworka).
- Do faktycznych zastosowań stosuje się klasy z końca hierarchii, nigdy bazowe.
- Dziedziczenie z klasy abstrakcyjnej wymusza na klasie pochodnej zaimplementowanie konkretnych konstruktorów. Dzięki czemu mamy pewność, że każdy obiekt klasy pochodnej będzie mógł zostać stworzony w ten sam sposób. To bardzo ułatwia np. atuomatyzację tworzenia obiektów.
Ostatni punkt jest dość istotny, bo CrudOperationResultBase
stało się klasą bazową dla DataPageResult<T>
, czyli klasy zawierającej dane wypełniające grida. Klasa ta wygląda teraz tak:
Czyli poza starym konstruktorem obsługującym “pozytywny” przypadek otrzymania danych z bazy, doszedł nowy konstruktor z klasy bazowej, który pozwala na zwrócenie informacji o błędzie.
Zastanawiałem się przez pewien czas, czy to dobry pomysł, czy nie lepiej byłoby tworzyć CrudOperationResult<DataPageResult<T>>
, ale stwiedziłem, że wymusi to zmianę interfejsów serwisowych, a co za tym idzie - konieczność poprawienia połowy aplikacji i większości testów, nie dając w zamian nic.
Nowa sygnatura ICrudServiceBase
Po tych zmianach trzeba poprawić sygnaturę ICrudServiceBase
oraz klasy go implementującej na taką:
Poprawka TransactionManagingInterceptora
Kontrakty za mną, teraz czas na implementacje. Ponieważ od zawsze uwazam, że w aplikacji powinno być jedno miejsce na komunikację z bazą oraz jedno miejsce na łapanie wyjątków, od początku w PizzaMVC stosuję takie podejście. Dzięki uprzejmości Autofaca i jego kompatybilności z Windsorowymi interceptorami powstała klasa TransactionManagingInterceptor
. Autofac opakowuje wywołania wszystkich metod serwisów oznaczonych atrybutem Transactional
właśnie tym interceptorem. (To jest przyczyna, dla której metody CrudServiceBase
są wirtualne.) Interceptor zaś:
- Sprawdza, czy jest już otwarta transakcja w ramach obecnego UoW, jeśli nie, to ją otwiera.
- Wykonuje kod opakowanej metody.
- Zatwierdza transakcję (a przynajmniej próbuje).
Jeśli podczas powyższych operacji nastąpił wyjątek, to do tej pory interceptor go łapał, logował… i rzucał dalej. Przy okazji StaleObjectException
z NHibernate opakowywał w mój własny OptimisticConcurrencyException
. Szczerze mówiąc, nie wiem, co piłem, gdy to wymyśliłem, ale na wszelki wypadek od dzisiaj niczego mocniejszego od herbaty nie ruszam.
Teraz czas na dobrą zmianę - w przypadku wystąienia wyjątku należy:
- Jeśli metoda zwraca
CrudOperationResult
,CrudOperationResult<T>
alboDataPageResult<T>
- utworzyć obiekt tego typu z odpowiednim statusem błędu oraz komunikatem. - W przypadku innych metod (a takie mogą się przecież znaleźć w serwisie), po prostu rzucić wyjątek dalej.
Wygląda to tak:
Bądźmy szczerzy - to nie jest piękny kod. Ale też trudno to zrobić dużo lepiej, bo throw
niestety musi być w catch
. :(
W każym razie - nieważne, czy czytamy dane, wstawiamy, kasujemy czy aktualizujemy, InvocationHelper
ma utworzyć odpowiedni obiekt wynikowy. Robi to tak:
Piękno prostoty w całej okazałości - parę linijek banalnej refleksji i nie trzeba mieć try-catcha powielającego praktycznie ten sam kod (otwarcie transakcji, właściwa akcja, commit transakcji, obsługa błędów i ifologia) w każdej metodzie serwisu. Zaoszczędzone dziesiątki liniek nikomu niepotrzebnego kodu.
Warstwa prezentacji
Do tej pory warstwa prezentacji otrzymywała wyjątek, więc w przypadku zwykłych akcji łapał go UniversalExceptionFilter
, a w przypadku żądań służących do pobrania danych do grida albo usunięcia rekordu, zajmowało się tym jQuery. Po zmianie interceptora, nie ma już wyjątków, więc trzeba ręcznie obsłużyć zwrócone statusy. Zmiany w warstwie prezentacji nie były duże. Po prostu dodałem kilka komunikatów o błędach do zasobów aplikacji. A w akcjach kontrolera CrudControllerBase
sprawdzam, czy operacja się powiodła. Jeśli tak, to pokazuję dane/przekierowanie. Jeśli nie, to wyświetlam komunkat o błedzie. W przypadku JSONa, zwracam odpowiedni komunikat i ustawiam Response.StatusCode
na HttpStatusCode.BadRequest
, aby jQuery wiedziało, że ma wyświetlić błąd.