REST API, Jeden kontroler wiele serwisów?

REST API, Jeden kontroler wiele serwisów?
EN
  • Rejestracja:ponad 3 lata
  • Ostatnio:około 2 lata
  • Postów:3
0

Hej,
Utknąłem w pewnych kwestiach przy przygotowywaniu prywatnego projektu (Dziennik szkolny), nie chciałbym na starcie pomieszać kilku rzeczy żeby pod koniec musieć wszystko sprzątać.

Nie do końca rozumiem pewnych kwestii.

  1. Czy jeden kontroler może (czy jest to wskazane) korzystać z kilku serwisów?
    Mam StudentController, w którym chciałbym udostępniać listę oceń ucznia
    Pod adresem students/{id}/grades, do tego potrzebuję skorzystać z serwisu, który dostarczy mi listę tych ocen

Przykład

Kopiuj
        public StudentController(IStudentService studentService, IGradeService gradeService)
        {
            _studentService = studentService;
            _gradeService = gradeService;
        }

        [HttpGet("{StudentId}/grades")]
        public async Task<IActionResult> GetGrades([FromRoute] GetStudentGradesRequest getStudentRequest)
        {
            var result = await _gradeService.GetStudentGrades(getStudentRequest);

            return result.Match<IActionResult>(error => error switch
            {
                DataNotFoundError _ => NotFound(error),
                _ => StatusCode(StatusCodes.Status500InternalServerError, error)
            }, _ => Ok(result.Data));
        }

    public class GetStudentGradesRequest
    {
        public int StudentId { get; set; }
    }

Czy powinienem do tego zastosować jakieś inne podejście w kwestii wrzucania serwisów do kontrolera? Jest jakiś bardziej pożądany/odpowiedni sposób?

  1. Validacja id przy PUT

W tym samym kontrolerze wystawiam również endpoint PUT przyjmujący id i obiekt z nowymi danymi ucznia, w projekcie korzystam z FluentValidation i chciałbym zwrócić odpowiednią wiadomość, gdy użytkownik spróbuje uderzyć w endpoint z id < 1

Kopiuj
        [HttpPut("{id:int:min(1)}")]
        public async Task<IActionResult> Update(int id, [FromBody]UpdateStudentRequest studentData)
        {
            var result = await _studentService.Update(id, studentData);

            return result.Match<IActionResult>(error => error switch
            {
                DataNotFoundError _ => NotFound(error),
                _ => StatusCode(StatusCodes.Status500InternalServerError, error)
            }, _ => Ok());
            
        }

Mam na to kilka pomysłów, ale żaden mnie nie satysfakcjonuje

  • Nie informować użytkownika o tym, a korzystając (jak teraz) z constraintów uniemożliwić korzystanie z PUT dla takiego id
  • Zrobić validację na początku metody Update (if id < 1 ...)
  • Utworzyć walidację dla dto (Student) i po zmapowaniu sprawdzać czy spełnia warunki
  • Pozwolić użytkownikowi uderzać w bazę i potraktować to tak samo jakby próbował dostać danę o uczniu z id, którego nie ma w bazie (notfound)
Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
8

Jeśli przewidujesz że skończy się to dużą ilością serwisów wstrzykiwanych do kontrolerów, to najlepiej byłoby to rozbić. Z tym że ja bym w takim przypadku radził zastosowanie wzorca mediator, i delegowanie każdej operacji do odpowiedniego request handlera. Wtedy Twój kontroler jest "głupi" i nie interesuje go jak coś zrobić, tylko aby wysłać polecenie zrobienia czegoś. Jedyne za co wtedy kontroler jest odpowiedzialny to wysłanie odpowiedniego polecenia, oraz zwrócenie odpowiedniego statusu.

https://github.com/jbogard/MediatR


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:dzień
  • Postów:5109
4

Czy jeden kontroler może (czy jest to wskazane) korzystać z kilku serwisów?

Generalnie nie jest to coś złego (imo), ale może od razu chcesz iść w kierunku MediatRa?

jakiś koncept przykład z neta

Kopiuj
[HttpGet("{id}")]
public async Task<Order> Get(int id) 
{
	var user = await _mediator.Send(new GetUserDetailQuery(id));
	
	if user == null 
	{
		return NotFound();
	}
	
	return Ok(user);
}
Kopiuj
public class GetUserDetailHandler : IRequestHandler<GetUserDetailQuery, UserDto> 
{
   	private readonly IUserRepository _userRepository;
    private readonly IUserMapper _userMapper;
    
    public GetUserDetailHandler(IUserRepository userRepository, IUserMapper userMapper)
    {
    	_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
    	_userMapper = userMapper ?? throw new ArgumentNullException(nameof(userMapper));
    }
    
    public async Task<UserDto> Handle(GetUserDetailQuery request, CancellationToken cancellationToken)
	{
		var user = await _userRepository.GetAsync(u => u.Id == request.Id)
		
		if (user == null)
		{
			return null; // wtf 
		}
		
		var userDto = _userMapper.MapUserDto(user);
		return userDto;
	}
}

W tym samym kontrolerze wystawiam również endpoint PUT przyjmujący id i obiekt z nowymi danymi ucznia, w projekcie korzystam z FluentValidation i chciałbym zwrócić odpowiednią wiadomość, gdy użytkownik spróbuje uderzyć w endpoint z id < 1

Nie informować użytkownika o tym, a korzystając (jak teraz) z constraintów uniemożliwić korzystanie z PUT dla takiego id
Zrobić validację na początku metody Update (if id < 1 ...)
Utworzyć walidację dla dto (Student) i po zmapowaniu sprawdzać czy spełnia warunki
Pozwolić użytkownikowi uderzać w bazę i potraktować to tak samo jakby próbował dostać danę o uczniu z id, którego nie ma w bazie (notfound)

Ja osobiście stosując wyżej wymienione podejście, to w metodzie Handle miałem również jakiś Validate, który np. walił do bazki, sprawdzał czy otrzymane daje są sensowne oraz spójne z systemem.

edytowany 10x, ostatnio: WeiXiao
Zobacz pozostałe 2 komentarze
Aventus
Rozumiem :D
EN
Od początku nie stosowałem cqrs, projekt nie jest na późnym etapie, chociaż trochę zmian by mnie czekało, jak się ma sam MediatR bez cqrs? Nie wiem czy wdrażać po prostu cqrs i pójść do końca tą ścieżką
WeiXiao
jak się ma sam MediatR bez cqrs? imo to MediatR chyba można też traktować jako taką nakładkę czy może konwencje/pomysł jak poukładać projekt.
WeiXiao
chociaż to też jest ciekawa kwestia, na reddicie :) nierzadko widziałem dyskusje typu po co mediatr którego zadaniem jest przekierowanie inputu do handlera, czyli właściwie tego samego co robi ASP, http -> controller. Ja uznałem że MediatR = konwencje i pewien pomysł na to jak może wyglądać projekt, chociaż to chyba temat podobny do design patternów, bo niby nie narzucają implementacji, a bardzo często(?) ludzie mają jakąś konkretną na myśli.
somekind
IUserMapper - strach się bać, co to za potwór. Do tego repozytorium w kontrolerze. A na domiar złego te średnio potrzebne ArgumentNullException.
VA
  • Rejestracja:ponad 7 lat
  • Ostatnio:5 dni
4

Proponowanie MediatR w sytuacjach takiego typu to nie jest żadne rozwiązanie bo tak jak już w komentarzu dopisał @WeiXiao - to jest wyłącznie przeniesienie potencjalnego problemu w inne miejsce.

Kontroler może korzystać z wielu serwisów, ale prawdę mówiąc im mniej tym lepiej.

Dla mnie te 2 konteksty powinny zostać rozdzielone. Wystawiłbym dwa osobne kontrolery do obsługi danych studenta oraz ocen i zaliczeń. A może nawet więcej bo czym innym są np. dane osobowe studenta a czym innym przypisanie do grup, obsługa ewentualnych płatności itd.

EN
Czyli sytuacja, w której jeden kontroler korzysta z 5 serwisów jest relatywnie gorsza od jednego kontrolera rozbitego na pięć mniejszych, korzystających z pojedynczych serwisów?
VA
To zależy. Kontroler tak jak już wspomniał @Aventus powinien być głupi i jego zadaniem powinno być oddelegowanie żądania do dalszego przetwarzania (plus może jakaś wstępna walidacja) oraz zwrócenie wyników. Jeśli ten kontroler korzysta z wielu serwisów bo buduje złożone viewmodele to jest to problem ponieważ on nie powinien tego robić. Jeśli natomiast ten kontroler wystawia metody udostępniające funkcjonalności różnych serwisów to trzeba się zastanowić czy tak na pewno nie ma zbyt szerokiej odpowiedzialności
Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
3

@var: nie zgadzam się z takim uproszczeniem, tak samo jak ze spotykanym przez @WeiXiao w internecie argumentem. Wprowadzenie wzorca mediator (MediatR to tylko biblioteka) i co za tym idzie odpowiednich handlerów to również wprowadzenie SRP, bo nagle od każdego procesu biznesowego mamy wyspecjalizowaną klasę, która również przyjmuje mniej zależności niż taki kontroler bo jedyne zależności to te związane z tym konkretnym procesem. Poza to część serwisów może nam wtedy zniknąć bo ich logikę można umieścić właśnie w handlarze. W dłuższej perspektywie znacznie to ulepsza architekturę w projekcie i czyni czytanie kodu dla konkretnego procesu znacznie czytelniejszym.

Tak więc powiedzenie że wprowadzenie mediatora to wyłącznie przeniesienie problemu w inne miejsce, czy też robienie tego samego co robi kontroler, to albo nie zrozumienie czym tak naprawdę jest wzorzec mediator i co wnosi, albo nadmierne upraszczanie.

Wystawiłbym dwa osobne kontrolery do obsługi danych studenta oraz ocen i zaliczeń. A może nawet więcej bo czym innym są np. dane osobowe studenta a czym innym przypisanie do grup, obsługa ewentualnych płatności itd.

Poza tym zwróć uwagę że tylko autor wątku wie o jakiej skali mowa. Pisanie do obsług płatności, obsługi zaliczeń itp. to już eksperyment myślowy bo nie wiadomo czy coś takiego będzie potrzebne. A nawet jeśli, to nadal można polemizować gdzie należało by umieścić endpoint do pobrania ocen studenta. A patrząc tylko na ścieżkę, np. students/{StudentId}/grades wydaje się mieć sens. Albo i nie. Tu akurat można polemizować jak już wspomniałem.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
EN
Skala nie będzie wykraczać poza zwykły dziennik szkolny, dodanie ocen, planu, ustalenie grup zajęciowych, sprawdzenie średniej itp. reszta kwestii jak np. opłata czesnego nie będzie wprowadzona (przynajmniej nie planuje)
JU
  • Rejestracja:około 22 lata
  • Ostatnio:około miesiąc
  • Postów:5042
4

Rozwiązanie z mediatorem wydaje się ciekawe z tego względu, że kontroler ma mało zależności. Nie próbowałem takiego podejścia, więc nie wypowiem się "uczciwie", jednak wydaje mi się, że może powstać pewien problem. Chociaż wszystko zależy od punktu siedzenia. W tym momencie jedna klasa jest odpowiedzialna za jedną operację. Z jednej strony mamy SRP, ale z drugiej strony, czy nie jest to może pewna nadgorliwość? Wychodziłoby, że do zarządzania uczniami (dodawanie, modyfikowanie, usuwanie) potrzebowalibyśmy 3 klas zamiast jednej w stylu StudentService, która miałaby te 3 operacje zaimplementowane.

Poza tym nie musimy wstrzykiwać serwisu w konstruktorze. Można to zrobić wtedy, kiedy naprawdę jest potrzebny w taki sposób:

Kopiuj
[HttpPost("endpoint")]
public async Task<IActionResult> PostDaSheet([FromBody]SheetDto dto, [FromServices]ISheetService service)
{

}
Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
0

@Juhas: oczywiście, jak to ze wszystkim w programowaniu bywa- to zależy ;) Dałem okejke za propozycję ze wstrzykiwaniem serwisów bezpośrednio do action methods, mimo że sam nie jestem fanem wstrzykiwania zależności gdziekolwiek poza konstruktorami.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
VA
  • Rejestracja:ponad 7 lat
  • Ostatnio:5 dni
3

Odnosiłem się do użycia biblioteki MediatR.
Sam wzorzec mediatora również nie rozwiązuje problemu, który może być bardziej skomplikowany od tego jaki został zaprezentowany.
Wielokrotnie widziałem konstruktory-potwory które przyjmowały po 15 zależności, które w teorii nie powinny być ze sobą powiązane. Ukrycie ich i przerzucenie gdzie indziej nie stanowi wtedy żadnego rozwiązania problemu.

Ale tak - zbyt daleko wybiegam myślami.

Co do samej nomenklatury - handler czy service na pewnym poziomie koncepcyjnym może być dokładnie tym samym

EN
  • Rejestracja:ponad 3 lata
  • Ostatnio:około 2 lata
  • Postów:3
0
Juhas napisał(a):

Wychodziłoby, że do zarządzania uczniami (dodawanie, modyfikowanie, usuwanie) potrzebowalibyśmy 3 klas zamiast jednej w stylu StudentService, która miałaby te 3 operacje zaimplementowane.

Jeżeli chodzi o zarządzanie bezpośrednio uczniem to tak, ale co gdy musimy chcemy dostać jego oceny lub inne dane 'dzieci'? To też umieściłbyś w StudentService?

JU
To zależy... Od konkretnego problemu.
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
3
enxnet napisał(a):
  1. Czy jeden kontroler może (czy jest to wskazane) korzystać z kilku serwisów?

Może korzystać z wielu zależności, ale to szybko prowadzi do powstania god-objectu z mnóstwem zależności. Kontroler jest często wejściem dla wielu różnych przypadków użycia, każdy z nich może wymagać zupełnie innych zależności, a kontroler będzie musiał mieć wszystkie.
No chyba, że będziemy robić kontrolery z pojedynczymi akcjami. Tylko to leczenie objawu, a nie problemu.

A druga sprawa jest taka, że jeśli coś się nazywa Service to zazwyczaj jest to smrodek - taka klasa robi wszystko, i łamie wszystkie możliwe dobre praktyki. Klasy powinny być małe, a ich nazwy odpowiadać temu, co robią, a nie być tak generyczne i nic niemówiące jak "service".

Mam na to kilka pomysłów, ale żaden mnie nie satysfakcjonuje

  • Nie informować użytkownika o tym, a korzystając (jak teraz) z constraintów uniemożliwić korzystanie z PUT dla takiego id
  • Zrobić validację na początku metody Update (if id < 1 ...)
  • Utworzyć walidację dla dto (Student) i po zmapowaniu sprawdzać czy spełnia warunki
  • Pozwolić użytkownikowi uderzać w bazę i potraktować to tak samo jakby próbował dostać danę o uczniu z id, którego nie ma w bazie (notfound)

Myślę, że jak najbardziej powinieneś zwracać 404, tylko w takim przypadku nie musisz nawet uderzać do bazy.

EN
  • Rejestracja:ponad 3 lata
  • Ostatnio:około 2 lata
  • Postów:3
0

Myślę, że jak najbardziej powinieneś zwracać 404, tylko w takim przypadku nie musisz nawet uderzać do bazy.

Lepiej sprawdzić to id w jakimś handlerze lub bezpośrednio przed wykonaniem zapytania?

edytowany 1x, ostatnio: somekind
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:około 5 godzin
  • Lokalizacja:Wrocław
2

Im wcześniej tym lepiej, więc najlepiej na poziomie akcji kontrolera. Jeśli "{id:int:min(1)}" wystarcza, to zostaw. Możesz też id zmienić na typ bez znaku. Możesz walnąć ifa na początku akcji.

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)