devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
5 minut

Trochę inna organizacja kodu w ASP.NET MVC


30.06.2011

ASP MVC 3 jest w dużej części spoko – znajdą się elementy bardzo irytujące, ale ogólnie mogę powiedzieć że jestem z pracy z tym frameworkiem raczej zadowolony. Denerwuje mnie jednak to, że pracując nad jedną daną akcją w jakimś kontrolerze muszę śmigać po kilku plikach:

  • plik kontrolera
  • plik z routingiem
  • plik z modelem parametru akcji
  • plik z modelem zwracanym przez akcję
  • plik z mapowaniami AutoMappera
  • plik widoku .cshtml
  • plik skryptów .js
  • … o czymś zapomniałem?

Jakiś czas temu postanowiłem wypróbować alternatywne podejście do organizacji kodu w swoim projekcie webowym…

[Przyszło mi ono do głowy zimą podczas przeglądania materiałów dotyczących frameworka FubuMVC, ale właściwie momentalnie zarzuciłem zapoznawanie się z tym rozwiązaniem, więc nie jestem w stanie na dzień dzisiejszy powiedzieć, jak poniższe podejście i praktyki Fubu “ze sobą korespondują“;)… co z kolei teraz staram się nadrabiać, eksperymentując ponownie z Fubu]

Najlepiej zobrazuje to przykład, żeby było wiadomo o co mi CHO. Poniżej przedstawiam kawałek kodu odpowiedzialny za akcję “administrator dodaje notatkę do firmy”:

  1:  public partial class CompaniesManagementController
  2:  {
  3:      [HttpPost]
  4:      public virtual ActionResult SaveNote(SaveNoteModel model)
  5:      {
  6:          var company = _session.Load<Company>(model.CompanyId);
  7:  
  8:          Note newNote = model.Map<Note>();
  9:  
 10:          company.AddNote(newNote);
 11:  
 12:          return Json(new SaveNoteResponse()
 13:                          {
 14:                              SavedSuccessfully = true,
 15:                              Message = MessagesContent.CompanyNote_SaveSuccess
 16:                          });
 17:      }
 18:  
 19:      public class SaveNoteModel
 20:      {
 21:          public int CompanyId { get; set; }
 22:  
 23:          [Required]
 24:          [DisplayName("Notatka")]
 25:          public string Content { get; set; }
 26:      }
 27:  
 28:      public class SaveNoteResponse
 29:      {
 30:          public bool SavedSuccessfully { get; set; }
 31:          public string Message { get; set; }
 32:      }
 33:  
 34:      public class Routes : IRouteProvider
 35:      {
 36:          public void RegisterRoutes(RouteCollection routes)
 37:          {
 38:              routes.MapRoute(
 39:                  "Admin_SaveNote",
 40:                  "Admin/ZapiszNotatke",
 41:                  MVC.Administration.CompaniesManagement.SaveNote()
 42:              );
 43:          }
 44:      }
 45:  
 46:      public class Mappings : IMappingProvider
 47:      {
 48:          public void RegisterMap()
 49:          {
 50:              AutoMapper.Mapper.CreateMap<SaveNoteModel, Note>();
 51:          }
 52:      }
 53:  }

Cóż tu widzimy? Jeden plik zawierający prawie wszystko, co jest potrzebne do oprogramowania jednej akcji. Kontroler jest partial – bo w innych plikach (które nazywam CompaniesManagement_SaveNote.cs, CompaniesManagement_DeleteCompany.cs etc…) piszę inne akcje, będące częściami tego samego kontrolera. Modele są zagnieżdżone – więc nigdy nie nastąpi żaden konflikt nazw z innymi mikroskopijnymi klaskami rozsianymi po projekcie. Tak samo definicje routingu oraz mapowań – każda akcja sama sobie definiuje pod jakim URLem chce się znajdować i w jaki sposób skorzystać z AutoMappera.

Dostrzegłem bowiem, że tak naprawdę NIC mi nie daje trzymanie modeli w jednym katalogu – bo nigdy nie potrzebuję dostępu do więcej niż jednego naraz. Tylko powoduje bałagan i zamieszanie podczas nawigacji. To samo z mapowaniami – i tak każda akcja ma własny model (lub dwa), więc i mapowania są wyłącznie dla niej charakterystyczne. A routing… No tutaj wiele już zależy od specyfiki danego systemu, ale generalnie podoba mi się swoboda, jaką daje przedstawione rozwiązanie: hierarchia URLi nie musi wcale pokrywać się z organizacją kontrolerów w “aree” oraz grupowanie akcji w kontrolery.

Wszystkie zależności wymagane przez wszystkie akcje kontrolera znajdują się w jednym konstruktorze, zdefiniowanym w głównym pliku CompaniesManagement.cs, który nie zawiera żadnych akcji. Zadaniem tego pliku jest przyjęcie zależności, dziedziczenie z odpowiedniej klasy bazowej oraz nałożenie atrybutów wspólnych dla wszystkich akcji (jak [Authorize]).

Okazało się, że taka organizacja kodu w projekcie webowym znacznie ułatwia mi pracę. Początkowo podchodziłem do tego bardzo sceptycznie, ale… po jakimś czasie wszystkie nowe funkcjonalności powstawały właśnie w ten sposób.

Zastanawiałem się czy nie pójść o krok dalej i nie “scustomizować” zachowania MVC tak, aby jeszcze bardziej wyeksponować akcję jako główny element rozwiązania webowego. Chodzi o to, aby każda akcja otrzymała osobny katalog, a w nim zarówno przedstawiony wyżej kod C#, jak i widok cshtml. Ale tego jeszcze nie wypróbowałem.

Co o tym myślicie? Tak jak napisałem – w moim przypadku takie coś naprawdę się sprawdza. Pliczki z akcjami są niewielkie i łatwo się po nich poruszać, a mają wszystko co potrzeba.

 

P.S.: Proszę po powyższym kodzie zbytnio nie jechać – ma on służyć jedynie demonstracji mojej koncepcji i nie jest żywcem wzięty z żadnego systemu.

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Notify of
procent

Uwaga, Gutek właśnie zwrócił mi uwagę, że klasa Mappings w powyższym kodzie nie ma metody – i faktycznie, to nie jest żadna magiczna sztuczka, tylko po prostu zapomniałem:). Tak jak pisałem, kod nie jest skopiowany z żadnego projektu, a dostosowany w Notatniku na potrzeby posta aby przedstawić ideę/koncepcję.
Posta poprawiam, ale do RSSów poszła głupota. Dzięki Gutek za uwagę!

Gutek

organizacja calkiem spoko, jednak ja troche inaczej do tego podchodze: controllers Name ViewModels ClassInput.css -> ClassInput { p FileInput F {get;set;} class FileInput {} } ClassDisplay.css Mappings … -> other folders and classes req by controller NameController.cs views ControllerName View.cshtlm GrouppedByControllerType ControllerName View.cshtml public css js app.js -> app.views.controllerName(); all views in one file ale pomysl by przezucic jeszcze view jest calkiem niezly rozbicia zas na akcje bym pewnie nie robil, raczej bym pomyslal o czyms takim http://lostechies.com/jimmybogard/2011/06/22/cleaning-up-posts-in-asp-net-mvc/

ja
ja

Zawsze mnie to zastanawia i w końcu muszę gdzieś o to zapytać. Load w NHibernate pobiera cały rekord w bazy po identyfikatorze. Rozumiem, jeżeli akurat w danej tabeli jest tylko kilka pól, np id, nazwa, to nie ma problemu. Co jednak w sytuacji, gdy w takiej tabeli jest pól przykładowo 30, a my potrzebujemy przerzucić do widoku tylko kilka wybranych, bo szczegóły w danej chwili nie są nam potrzebne? Używać get/load i później mapować to na na własne view modele? Czy w takich sytuacjach korzystać właśnie z Projections, którą udostępnia NHib? Wydaje mi się, że w celach wydajnościowych oczywiście należałoby… Read more »

Gutek

@ja load tworzy proxy tak samo jak get, roznica polega na tym: 1) get-> jezeli nie ma w cache i 2nd level cache entity, robi hit do bazy, jak nie ma entity to zwraca null, lub proxy 2) load-> tworzy proxy i nie uderza do bazy, jednak jak robi sie transaction commit i entity o danym ID nie istnieje w bazie danych zwracany jest wyjatek ogolnie powinno sie uzywac Get i Load jak chce sie miec proxy ale to czy get czy load zalezy od sytuacji. napewno nie powinno sie robic Criteria po ID – w sensie, nie powinno sie… Read more »

eric
eric

Podoba mi się takie rozłożenie kodu. Jednak gdy zapragniesz dodać kolejną akcję w innym pliku, to się posypie
na klasach do routngu i mappingu, bo nazwy te same. Interesuje mnie jeszcze, jak wywołujesz dodanie tych routingów i mappingów.
Gdyby to były tylko dwie klasy, ktore zapisałeś, to można z konstruktora. ale w takim przypadku? Chyba że w kolejnych akcjach jakoś można dodać kolejne mappingi/routingi do już tych istniejących klas. Prosiłbym o rozwinięcie tematu w tym kierunku.

procent

ja, To zależy od kontekstu, od systemu, od przewidywanego obciążenia, od ilości danych… Tutaj skupiłem się na całkowicie innej kwestii. 1) w takim scenriuszu użyłbym Get() a nie Load(), ale w kontekście posta nie ma to znaczenia 2) nic nie stoi na przeszkodzie aby mieć więcej niż jedną klasę mapowaną na tabelę Company – może być Company zawierająca właściwości dla wszystkich kolumn, a może być np CompanyWithNotes zawierająca tylko id, nazwę i referencję do notatek Z tym że nie ma sensu bawić się w takie rozróżnienia w systemie, dla którego nie zrobi to żadnej różnicy. Z kolei przy pobieraniu więcej… Read more »

procent

eric,
Masz rację w takim przypadku klasy mapowań i routingu zrobiłbym partial albo tworzył o unikalnych nazwach, np RoutingForSaveNote, MappingsForSaveNote etc.. Ale pewnie stanęłoby na partialach.

Rejestrację mapowań i routingu robię w jakimś app_start czy innym bootstraperze – wszystko automatycznie rejestruje mi się w Autofac, więc gdzieś przy starcie aplikacji mam coś takiego:
foreach (var m in container.Resolve<IEnumerable<IMappingProvider>>())
{
m.RegisterMap();
}

Szymon Pobiega

Awesome:) Bardzo mi się podoba. Chociaż ja osobiście nie wrzucałbym tego w partial classę opakowującą, tylko wrzucił jako osobne klasy do jednego katalogu. Ale idea jest cudowna:)

procent

Szymon,
Od 2 dni zagłębiam się bardziej w Fubu (w sumie napisanie tego posta mnie skłoniło do przyjrzenia się Fubu bliżej:) ) i tam wszystko opiera się na podejściu: coś takiego jak kontroler w ogóle nie jest potrzebne, ważne sa tylko AKCJE. I ta idea, jak i implementacja, BARDZO mi się podobają, polecam rzucenie okiem (pewnie coś o tym skrobnę za jakiś czas).

Łukasz
Łukasz

"Zastanawiałem się czy nie pójść o krok dalej i nie "scustomizować" zachowania MVC tak, aby jeszcze bardziej wyeksponować akcję jako główny element rozwiązania webowego. Chodzi o to, aby każda akcja otrzymała osobny katalog, a w nim zarówno przedstawiony wyżej kod C#, jak i widok cshtml. Ale tego jeszcze nie wypróbowałem." To bardzo ciekawe podejście a ASP.NET MVC, coś podobnego jest u konkurencji chyba w Zend Framework, gdzie można dowolnie ustawiać sobie strukturę katalogów. Takie rozwiązanie które zaproponowałeś to zajebiście czytelna i skompresowana struktura projektu. Niby MapAreas ma poprawiać czytelność ale nadal jak widać tu http://haacked.com/archive/2008/11/04/areas-in-aspnetmvc.aspx na zrzucie struktura jest to… Read more »

procent

Łukasz,
Areas to moim zdaniem trochę pomyłka. Co prawda korzystam z nich, ale… one po prostu dodają kolejny poziom organizacyjny, nie zajmując się źródłem problemu. A źródłem problemu jest zbyt chaotyczna i skomplikowana struktura. Po wprowadzeniu Areas nadal mam burdel – tyle że każdy burdel zamknięty jest we własnym folderze. Kilka odseparowanych od siebie burdeli może i jest lepsze niż jeden mega-burdel, ale im dłużej się nad tym zastanawiam tym mniej to do mnie przemawia.

Whut

Hej, w moim projekcie też się zastanawialiśmy nad organizacją katalogów tak, by wszystko dotyczące jednego "feature" było w jednym miejscu. Wygrał układ, który dokładniej opisałem tutaj: http://dotnet.uni.lodz.pl/whut/post/2011/07/03/Better-ASPNET-MVC-folder-structure.aspx Pokrotce wygląda on tak: Areas\ // wolałbym nazwę "Features", ale Areas jest wymagane przez ASP.NET MVC AREA#1\ Views\ // ten katalog też istnieje tylko po to by zadowolić ASP.NET MVC CONTROLLER#1\ // bezpośrednio w tym katalogu są widoki Code\ // tu jest controler, modele, mapowania, itp. Content\ // tu js i css Images\ CONTROLLER#2\ Code\ Content\ Images\ … Shared\ // zwykle tu nic nie ma Code\ Content\ Images\ AREA#2\ … Views\ CONTROLLER#3\ …… Read more »

Whut

BTW. twoja koncepcja przypomina mi MVVM: partial klasa z jedna akcją kontrolera i modelami do niej to odpowiednik jednego ViewModelu.

Whut

Jeszcze jeden komentarz: rozbicie akcji kontrolera na oddzielne pliki jest fajny: zwiększa czytelność i mniej kodu jest do przejrzenia, by zrozumieć co się dzieje. Ale IMO prostszym rozwiązaniem jest rozbicie kontrolera na oddzielne klasy, czyli np. zamiast PersonController z metodami SaveNew/Edit dwa kontrolery: SavePersonController i EditPersonController.

Ten opisany przeze mnie wyżej układ katalogów jest pod takie podejście do kontrolerów: byłby tam area Persons z kontrolerami SaveController i EditController

procent

Whut,
Czy jeden kontroler partial, czy osobne kontrolery – nie ma to dla mnie znaczenia. Kontroler jest tylko paczką z akcjami.
A Twoja koncepcja mi pasuje, chociaż wydaje mi się że nie idzie o wiele dalej niż to co napisałem. Ale faktycznie akie wykorzystanie "Area" jest bardziej wygodne niż "standardowe", zalecane przez MS.
A nawigacja w kodzie nie jest problemem – Resharper to załatwia. Bez Resharpera to nie wiem czy bym w ogóle został przy VS:).

Whut

Fakt, jedyne praktyczna różnica to to, że zamiast partial klas używam areas:)

"Kontroler jest tylko paczką z akcjami" – muszę spojrzeć na FuBu żeby to poczuć

pawelek

Witam,

u nas podobnie jak u WHUT’a.

Areas/
Nazwa Featura (zazwyczaj opcji w menu)/
Views/
Nazwa Controllera/
Code, Content, pliki cshtml

Projekt z testami odpowiada powyższemu tylko do 2 poziomu.
Areas/
Nazwa Feature/
pliki .cs z testami

Natomiast w projekcie odpowiadającym warstwie logiki.
Domain/
główne klasy + mapowiania nHibernate

Features/
Nazwa Featura (która niestety nie odpowiada Nazwie Featura z apliacji MVC)/
głównie pliki z handlerami agathy

W sumie tyle. Staramy się dzielić kod w stylu: jeśli masz jakiś Feature w aplikacji
to jest to osobny folder.

procent

pawelek,
Ayende pisał kiedyś o czymś podobnym, dodatkowo ekxplorując podziału architektury oprogramowania na "features and concepts": http://ayende.com/blog/4333/effectus-isolated-features

whut
whut

Właśnie się spotkałem z pawelkiem, okazało się, że siedzi biurko obok:)

Pomysł na podział jest z bloga Ayende:)

PrzypadkowyObserwator
PrzypadkowyObserwator

[quote]
wolałbym nazwę "Features", ale Areas jest wymagane przez ASP.NET MVC
[/qoute]
[quote]
// ten katalog też istnieje tylko po to by zadowolić ASP.NET MVC
[/quote]
Można w prosty sposób pozbyć się tych ograniczeń, dziedzicząc ViewEngine i podstawiając mu swoje ścieżki. Taką technikę stosuje np. SharpArchitecture (https://github.com/sharparchitecture/Sharp-Architecture/blob/1.9.6.0/src/SharpArch/SharpArch.Web/Areas/AreaViewEngine.cs)
Pozdrawiam
H.

PrzypadkowyObserwator
PrzypadkowyObserwator

Jeszcze odnosząc się do zaprezentowanego pomysłu – podoba mi się, ale mam kilka uwag :) 1. Chyba w każdym większym projekcie najwięcej czasu poświęca się na nawigację pomiędzy plikami. Jestem użytkownikiem SharpArchitecture, gdzie wraz z dobrodziejstwem inwentarza przychodzi oddzielenie widoków od kontrolerów (osobne biblioteki), więc gonitwy mam dwa razy więcej :) 2. Naprawdę piszecie osobne modele dla wejścia i wyjścia każdej metody? Matko ile wy musicie mieć klas w projektach… A potem aktualizacja tego to musi być masakra… Przy klasach na których ‘cykl życia’ polega jedynie na istnieniu, kontroler ogranicza się do CRUD, a modele mam zwykle dwa – jeden… Read more »

procent

PrzypadkowyObserwator, Ano z ViewEngine racja, spoko tip. 1. Co do nawigacji – resharper to the rescue, ale i tak otwieranie kilku malutkich plików jednoczesnie (nawet jesli samo ich znalezienie jest banalne z R#) to dla mnie osobiscie pewien dyskomfort. 2. Klas mogę mieć i milion, co za różnica? Tekst to tekst. A problemu z aktualizacją nie do końca rozumiem. Aktualizacja czego musi być masakrą? Przecież nie trzeba nigdy zmieniać wszystkich klas naraz, jednocześniej, tylko każda z nich odpowiada za jeden malutki wycinek systemu i jest niezależna od reszty. No i te klasy są zwykle po prostu zbiorem get/set i niczym… Read more »

Przypadkowy Obserwator
Przypadkowy Obserwator

ad1 ahhh więc do tego używa się resharpera ;) Robiłem do niego kilka podejść i nigdy się nie przekonałem. Ale może trzeba jeszcze raz…
ad2 Co do ilości klas – sama w sobie nic nie zmienia, choć ma wpływ np. na kompilację. Dlaczego masakrą – dlatego że jak dodajesz jakieś pole do klasy biznesowej to musisz je później propagować na co najmniej 2 miejsca. A potem jeszcze reguły walidacji, testowanie itp…
Chyba że masz jakąś sztuczkę która to automatyzuje?
ad3 Jak już pisałem – pomysł mi się podoba, a w szczególności jeśli wymagania narzucają tak rozbudowaną strukturę routingu.
pozdrawiam
H.

procent

Przypadkowy Obserwator, 1. Przedstawianie R# jako "narzędzia do nawigacji" jest dla niego krzywdzące, ALE gdyby faktycznie tak było (tzn gdyby R# oferował wyłącznie nawigację w kodzie) to i tak byłby warty dzisiejszej ceny. A że przy okazji dostaje się milion razy więcej to tym lepiej:). 2. Wpływ liczby klas/plików na kompilację… no nie wiem czy powinniśmy w ogóle się takimi rzeczami przejmować, to dla mnie trochę przesada. A właśnie mnogość tych klas powoduje, że modyfikacja klasy biznesowej NIE WYMUSZA zmian w X innych miejsc – a tylko w tych, gdzie jest to konieczne, czyli dla ekranów które faktycznie tych nowych… Read more »

PrzypadkowyObserwator
PrzypadkowyObserwator

1. Co zabawne, licencję na R# mam – wygrałem na jakiejś konferencji, ale poważnie nie mogę się przekonać. Jestem przyzwyczajony do skrótów i własnych szablonów w VS, a R# zawsze mi wszystko rozwala. Pewnie gdyby go skonfigurować to dałby radę ale jakoś zawsze brakuje mi cierpliwości. A może strzelisz jakąś serię artykułów "Resharper dla starych wyjadaczy VS"? 2. Odnośnie liczby plików/klas – nie chciałbym żebyś mnie źle zrozumiał. Doskonale wiem, że każdy projekt ma inne wymagania, które mogą prowadzić do różnych ciekawych rozwiązań – podobnie jak przy dyskutowanym wcześniej routingu. Oczywiście nie jestem zwolennikiem jednego wielkiego modelu, wyczerpującego wszystkie przypadki… Read more »

procent

1) Taką serię kiedyś puściłem (w 3 odcinkach), co prawda było to dość dawno i tyczy wersji chyba 3.x, ale wszystko co tam jest pokazane nie straciło na aktualności, polecam http://www.maciejaniserowicz.com/?tag=/c%23+via+r%23 2) Patentu jako takiego nie mam, ale na pewno nie będę szedł w kierunku generacji kodu. Po prostu widzę szkic UI i dane jakie mają być na nim pokazane -> i tworzę dla niego model. Nie szukam czy taki lub podobny już gdzieś istnieje. Po prostu każdy ekran/widok ma swoją własną klasę. A co do kompilacji… znów się R# kłania:). Nie muszę kompilować co chwilę projektu bo resharper robi… Read more »

pawelek

Fakt zmiana biznesowa, pociąga za soba zmiany w DTO oraz modelu… Ale nie widzę innej możliwości.
Fajny tip z tym ViewEngine, ale jak mówił WHUT (przed chwila rozmawialiśmy) to jak zaglądał do kodu to słówko Views widział zahardkodowane w wielu miejscach. Sam nie patrzyłem więc to tylko taki assume :)

Natomiast refactoring tak wielu klas pociąga za sobą masakryczna robotę i spodziewać się można że czasem drobna zmiana przebiegająca przez cały projekt (jak wczorajsza zmiana typu ze string na AccountNumber) to kilka godzin roboty.

Pozdrawiam
P.

Whut

Właśnie sprawdziłem drugi raz, nie wiem, gdzie ja widziałem to "Views" czy "Areas" zahardcodowane w wielu miejscach. Teraz oprócz ViewEngine widzę je teraz tylko raz i to dodatkowo w klasie, która zdaje się, że jest unsupported.

A do komentarza wyżej: jak jest wiele modeli to jest łatwiej, IMO, tak jak pisał wyżej procent. My w naszej aplikacji mamy "masakryczną robotę" od czasu do czasu, bo oprócz wielu modeli mamy też wiele DTOsów, (to wynika z specyfiki projektu).

Moja książka

Facebook

Zobacz również