ASP.NET MVC i Unity
Nadejszła wiekopomna chwila – ASP.NET MVC już od jakiegoś czasu egzystuje jako oficjalny produkt, więc oto najwyższa pora na zapoznanie się z nim. Cierpliwie przeczekałem wszystkie ochy i achy, pokonałem chęć bycia "trendy";) i zabieram się za to dopiero teraz.
Na pierwszy ogień poszło wpasowanie w cały mechanizm kontenera Dependency Injection tak, aby kontrolery brały się właśnie z niego. Wklejenie gotowych kilku linijek kodu byłoby dość nudne, więc postanowiłem tym razem przedstawić swoje poszczególne kroki i myśli lęgnące się w mózgu podczas poznawania zupełnie nieznanego wcześniej kodu (co wcale, uprzedzam, nie oznacza, że będzie ciekawie!). Poniżej zatem wierne odzwierciedlenie mojej ścieżki od koncepcji do rozwiązania:
1) Najpierw należało zidentyfikować SKĄD te kontrolery się biorą. Bystre moje, niczym woda wiadomo gdzie, oczy, zauważyły, że każdy kontroler dziedziczy z klasy bazowej o niespodziewanej i zaskakującej nazwie... Controller. I to tyle czego oczekuję od Visual Studio, czas na przesiadkę do Reflectora. Po uruchomieniu tego cuda i wciśnięciu ctrl+o (czyli Open File) odszukałem dllkę zawierającą wszystkie tajemnice ASP.NET MVC, czyli:
(zawczasu dodałem ją do repozytorium projektu wiedząc, że mój wybór tym razem padnie na tą technologię)
2) Załadowawszy żądany zestaw (jakie megamądre stwierdzenia z mych rąk dziś płyną, a niech mnie!) zabrałem się za jego przeszukiwanie. Klawisz F3 otwierający panel wyszukiwania był w tym wypadku niezastąpiony. Niezwłocznie przystąpiłem do poszukiwań:
3) Jakże gładko poszło! Dwuklik na tym interesującym wierszu przeniósł mnie w panel nawigacji po bibliotekach i typach. Z ciekawości należało zanalizować pochodzenie klasy Controller, bardzo prawdopodobne było że w infrastrukturze ASP.NET MVC jest jakaś bardziej generalna klasa czy interfejs. A jakże:
Nie dość, że mamy ControllerBase, to jeszcze implementuje ona najwyższy w hierarchii interfejs IController. To musiało być to, byłem święcie przekonany że cokolwiek nie tworzyłoby instancji poszczególnych kontrolerów, zwracałoby je właśnie w postaci implementacji tego interfejsu. Kolejny dwuklik scentralizował mój świat na tym interfejsie, w którym mogłem się zagrzebać.
4) Zawartość interfejsu interesowała mnie co najwyżej średnio, co nie przeszkodziło jednak w chwilowym zboczeniu z drogi w celu dowiedzenia się, że jedyna odpowiedzialność kontrolera to "zrobienie czegoś" (w metodzie Execute()) mając do dyspozycji informacje o bieżącym żądaniu http oraz coścośtam związanego z url rewritingiem. Na szczęście Reflector posiada przycisk BACK, który pozwoli wygrzebać się z kodowego bagna i wrócić do odpowiedniego miejsca nawet jeśli bardzo damy się ponieść duchowi eksploratora:
Po powrocie do interfejsu kliknąłem prawym przyciskiem, a następnie wybrałem Analyze... zobaczymy gdzież to ów interfejs jest wykorzystywany.
5) W oknie analizatora rozwinąłem odpowiednie grupy i cóż się okazało? A-HA, mamy cię! To już nawet Gosiu by się domyślił, że IControllerFactory tworzy kontrolery:
Przeszedłem zatem w to miejsce aby kontynuować badania.
6) Po wciśnięciu ctrl+r, równoważnym z wybraniem Analyze z menu kontekstowego, ponownie znalazłem się w oknie analizatora. I jakież to ciekawe rzeczy ukazały się moim astygmatycznym ślepiom:
BINGO! Co prawda od razu pojawiła się myśl "hmm, dwa tak mocno powiązane ze sobą typy nazywające się - IControllerFactory oraz ControllerBuilder - tak, jakby robiły dokładnie to samo... ciekawe dlaczego?", ale tym razem poskromiłem ciekawość i zostawiłem inwestygację na kiedy indziej.
Czyli już wiem co trzeba wywołać, aby podmienić standardowy sposób tworzenia kontrolerów. Zostały pytania: GDZIE to zrobić, JAK się do owej metody dostać i CO jej przekazać?
7) Kolejne sekundy z Reflectorem przyniosły odpowiedzi na dwa pierwsze pytania:
W statycznym konstruktorze tworzona jest domyślna instancja Buildera, a właściwość Current właśnie ją zwraca. Nie mamy co prawda do czynienia z Singletonem, ale już znam odpowiedź na pytanie "JAK?". O tak: ControllerBuilder.Current.SetControllerFactory(...). A "GDZIE"? Najpewniej najstosowniejszym miejscem będzie kod wykonywany podczas uruchomienia aplikacji, więc domniemana lokalizacja to Application_Start() w Global.asax.cs.
8) Czas zmierzyć się z pytaniem trzecim: CO tam wrzucić? Aby na nie sensownie odpowiedzieć, należy przyjrzeć się istniejącym implementacjom IControllerFactory. A w tym przypadku - jedynej istniejącej implementacji.
Krótki rzut oka na zawartość klasy DefaultControllerFactory wystarczył do podjęcia decyzji: oto będzie właśnie matka mojej implementacji; za dużo mądrych rzeczy tam się musi dziać w tych wszystkich prywatnych składowych, aby wymyślać koło na nowo i próbować przekodować to samo w swoim, notabene wolnym, czasie. Zawęziłem zatem swoją analizę jedynie do metod zwracających IController – jako kandydatów do ewentualnego nadpisania. Idealna do tego celu okazała się metoda GetControllerInstance():
Jedyne co tak naprawdę robi oprócz sprawdzania argumentów i rzucania wyjątków to wywołanie Activator.CreateInstance(Type), czyli jest to perfekcyjnie zlokalizowany kawałek kodu do podmiany! Zauważyć warto, że oznaczona jest modyfikatorami "protected internal", których występowanie czasami BŁĘDNIE tłumaczone jest jako: "metoda widoczna jedynie dla klas zadeklarowanych w tym samym assembly i dziedziczących z jej właściciela”. Zapamiętać należy, że tak naprawdę stawiany tam jest operator OR, a nie AND! Poprawne wyjaśnienie: "metoda widoczna dla klas zadeklarowanych w tym samym assembly ORAZ dla klas dziedziczących z jej właściciela”. Tak więc możemy ją sobie spokojnie nadpisać.
9) OKej, posiadając te wszystkie informacje można zakodować co następuje:
1: public class UnityControllerFactory : DefaultControllerFactory
2: {
3: private readonly IUnityContainer _container;
4:
5: public UnityControllerFactory(IUnityContainer container)
6: {
7: _container = container;
8: }
9:
10: protected override IController GetControllerInstance(Type controllerType)
11: {
12: return (IController)_container.Resolve(controllerType);
13: }
14: }
Tyle zabiegów sprowadziło się do właściwie jednej linijki, uzupełnionej o:
1: protected void Application_Start()
2: {
3: ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory(Unity.Container));
4: RegisterRoutes(RouteTable.Routes);
5: }
, gdzie statyczna klasa Unity to mój własny helper.
I bardzo dobrze. Tym lepiej, że działa bez zarzutu!
To już koniec wycieczki, proszę wysiadać. Mam nadzieję, że przekonałem choć jedną "niewierzącą" osobę jak potrzebnym i wspaniałym narzędziem jest Reflector. Okazało się również, że o ile Google jest zdecydowanie najszybszym sposobem na znalezienie odpowiedzi, jednocześnie nie jest to najbardziej satysfakcjonująca i niosąca najwięcej wartości edukacyjnych droga.
Dopowiem jeszcze, że w rzeczywistości podróż ta trwała około kwadransa. Dopowiem też, że po tym wszystkim z ciekawości poprzeglądałem internet w poszukiwaniu lepszych rozwiązań i... zdziwiłem się jak bardzo można nakombinować, żeby skomplikować sobie życie. Znalazłem sporo alternatyw (bardzo zresztą podobnych), każda jednak jednak miała więcej linii kodu. Po co?
Najczęściej the simpler => the better.