Specjalizacja metody - problem z unboxingiem

0

Cześć, mam taki mechanizm, w którym używam czegoś na wzór specjalizacji metody:

bool Foo(object obj)
{
  return Bar(obj);
}

bool Bar(Type1 obj)
{
}

bool Bar(Type2 obj)
{
}

bool Bar<T>(T obj)
{
}

Oczywiście w momencie wykonywania kodu dokładnie wiadomo, jaki typ kryje się pod głównym obiektem, niestety zawsze w takiej sytuacji wywoływana zostaje metoda generyczna. Natomiast, jeśli zrobię taki mały myk:

bool Foo(object obj)
{
  dynamic dObj = Convert.ChangeType(obj, obj.GetType());
  return Bar(dObj);
}

to wszystko już pięknie śmiga. Trochę nie podoba mi się to rozwiązanie (być może martwię się na wyrost), ponieważ to jest WebAPI (.Net Core 2). I ten kod jest w filtrze, który wykonuje się przed zapuszczeniem każdego kontrolera. Martwi mnie tutaj narzut na konwersję i tu w przyszłości mogą powstać jakieś problemy wydajnościowe. Czy jest inny sposób, żeby to osiągnąć minimalnym narzutem?

0

A w jakim właściwie celu masz metodę generyczną i zestaw metod niegenerycznych o tej samej nazwie?

0

Chcę zrobić coś na zasadzie specjalizacji metody z C++. To ma działać tak, że w zależności od typu obiektu, jaki dostanie, odpala odpowiednią metodę. Dzięki temu unikam ifów:

if((obj.GetType()) == typeof(Klasa1))
else...

tylko wszystko załatwiam jedną instrukcją.

0

Co nie zmienia faktu że to jest brzydkie i zastanowiłbym się czy nie da się inaczej rozwiązać większego problemu który doprowadził do tego że potrzebujesz mieć wyspecjalizowane metody.

1
bool Foo(object obj)
{
  dynamic dObj = Convert.ChangeType(obj, obj.GetType());
  return Bar(dObj);
}

myślę że tu nawet wystarczy

bool Foo(object obj)
{
  return Bar((dynamic)obj);
}
0
neves napisał(a):

Co nie zmienia faktu że to jest brzydkie i zastanowiłbym się czy nie da się inaczej rozwiązać większego problemu który doprowadził do tego że potrzebujesz mieć wyspecjalizowane metody.

Ja wpadłem właśnie na takie rozwiązanie. Oto opis problemu:

Użytkownik aplikacji może posiadać jeden z trzech planów. Każdy plan pozwala na inne rzeczy (im droższy plan, tym można więcej).
Dlatego też zrobiłem sobie filtr, który jest uruchamiany przed kontrolerem. W filtrze sprawdzam, jaki plan powinien mieć użytkownik. Następnie tworzę sobie obiekt konkretnej klasy (BasicPlanGuard, ProPlanGuard itd), który ma określić, czy użytkownik może wykonać konkretną akcję.

PlanGuard najpierw rozpoznaje typ kontrolera, który ma zostać obsłużony. I tu jest moje rozwiązanie. Mam kilka kontrolerów zależnych od planów i każdy kontroler jest sprawdzany w osobnej metodzie. Np. jeśli to kontroler klientów, metoda np. może sobie sprawdzić, czy użytkownik może dodać kolejnego klienta.

Ja to rozwiązałem tak. Wydaje mi się, że jest to czyste i łatwe rozwiązanie.

0

No dobrze, a czemu nie wykorzystasz do tego wbudowanego we framework mechanizmu ról i atrybutu Authorize?

0

Wykorzystuję polityki różne i nimi ograniczam dostęp do poszczególnych elementów serwisu. Natomiast w planach jest np. tak, że użytkownik może mieć tylko 10 klientów. Normalnie planów używam do tego, żeby sprawdzić, czy użytkownik może np. dodać klienta. Ale czy może dodać KOLEJNEGO - to sprawdza mój filtr.

0

No ale to chyba jakaś strategia biznesową powinna to sprawdzać. Poza kontrolerem.

0

No to właśnie sprawdza to poza kontrolerem. To jest przed odpaleniem kontrolera w filtrze.

0

:) OK

0

Rozwiązanie z rzutowaniem jest ok o ile nie masz dużo (bardzo dużo) wywołań na sekundę

1

Co do problemu z implementacją to widzę dwa wyjścia:

  1. Usunąć Bar<T> - no bo niby czemu to ma służyć, skoro i tak piszesz metody specyficzne dla obsługiwanych przez siebie typów kontrolerów.
  2. Usunąć (object obj), wszystkie pozostałe metody nazwać Foo i pozwolić działać normalnemu przeciążaniu metod bez kombinowania.

Ale moim zdaniem główny problem tutaj to ogólna koncepcja:

  1. Czemu jeden filtr ma sprawdzać różne reguły biznesowe dla różnych kontrolerów? Każda weryfikowana akcja powinna mieć swój kontroler. Wiązanie każdego PlanGuarda z każdym kontrolerem to ścisłe wiązanie, które skutkuje trudnościami w zmienianiu i rozwijaniu takiego kodu. Dodając nowy kontroler będziesz musiał zmienić wszystkie guardy. Dodając nowegoguarda będziesz musiał obsłużyć w nim wszystkie kontrolery. Brzmi groźnie. I łamie SRP i OCP.
  2. Czy te wszystkie PlanGuardy są na pewno potrzebne? Jeśli poszczególne plany różnią się jedynie liczbami operacji danego rodzaju możliwymi do wykonania przez użytkownika, to przecież wystarczy umieścić te limity w jakichś claimsach zalogowanego użytkownika, a potem jeden "Guard' może po prostu porównać dany limit z obecnym wykorzystaniem i zezwolić na wykonanie akcji bądź nie.
0
somekind napisał(a):

Co do problemu z implementacją to widzę dwa wyjścia:

  1. Usunąć Bar<T> - no bo niby czemu to ma służyć, skoro i tak piszesz metody specyficzne dla obsługiwanych przez siebie typów kontrolerów.

To musi pozostać dla wszystkich innych kontrolerów nieobsługiwanych przez dany plan. To wygląda mniej więcej tak:

class BasicPlanGuard: PlanGuard
{
  protected override bool CheckPlan(ActionExecutingContext context, ControllerActionDescriptor descriptor)
  {
    return CheckController((dynamic)context.Controller, descriptor);
  }

  bool CheckController(ClientsController c, ControllerActionDescriptor descriptor)
  {
    //sprawdza np., czy użytkownik może dodać kolejnego klienta
  }

  bool CheckController(PremiseController c, ControllerActionDescriptor descriptor)
  {
    //sprawdza np., czy użytkownik może dodać kolejny lokal
  }

  bool CheckController<T>(T c, ControllerActionDescriptor descriptor)
  {
     return true; //tu idą wszystkie inne kontrolery, które nie mają żadnych ograniczeń dla danego planu
  }
}
  1. Usunąć (object obj), wszystkie pozostałe metody nazwać Foo i pozwolić działać normalnemu przeciążaniu metod bez kombinowania.

No właśnie nie da się tak, co opisałem kodem powyżej :)

Ale moim zdaniem główny problem tutaj to ogólna koncepcja:

  1. Czemu jeden filtr ma sprawdzać różne reguły biznesowe dla różnych kontrolerów? Każda weryfikowana akcja powinna mieć swój kontroler. Wiązanie każdego PlanGuarda z każdym kontrolerem to ścisłe wiązanie, które skutkuje trudnościami w zmienianiu i rozwijaniu takiego kodu. Dodając nowy kontroler będziesz musiał zmienić wszystkie guardy. Dodając nowegoguarda będziesz musiał obsłużyć w nim wszystkie kontrolery. Brzmi groźnie. I łamie SRP i OCP.

No właśnie nie. Dodając nowy kontroler, nie muszę zmieniać nic w Guardach ze względu na tą generyczną wersję metody CheckController. Dlatego też nie wiążę PlanGuarda z każdym kontrolerem, tylko PlanGuard obsługuje tylko te kontrolery, które mają mieć jakieś ograniczenie. Akcje dla konkretnych kontrolerów są sprawdzane w metodzie CheckController w taki sposób:

bool CheckController(ClientsController c, ControllerActionDescriptor descriptor)
{
  if(descriptor.ActionName == nameof(c.Post))
  {
    //oznacza, że użytkownik próbuje dodać nowego klienta, więc sprawdzam tu, czy może zgodnie ze swoim planem
  }
}

Jeśli dodam nowy plan kiedyś, to muszę obsłużyć w nim tylko konkretne akcje dla konkretnych kontrolerów.

  1. Czy te wszystkie PlanGuardy są na pewno potrzebne? Jeśli poszczególne plany różnią się jedynie liczbami operacji danego rodzaju możliwymi do wykonania przez użytkownika, to przecież wystarczy umieścić te limity w jakichś claimsach zalogowanego użytkownika, a potem jeden "Guard' może po prostu porównać dany limit z obecnym wykorzystaniem i zezwolić na wykonanie akcji bądź nie.

Jest to jakieś rozwiązanie, ale moim zdaniem zbyt ubogie. Moje rozwiązanie było czasowo porównywalne kosztowo do Twojego. Natomiast umożliwia mi dużo więcej. Być może za miesiąc jakiś plan w ogóle nie będzie mógł wykorzystać jakiejś akcji albo będzie musiał zareagować w specyficzny sposób. To wszystko mógłbym oczywiście zaszyć w claimsach, ale rozwiązanie z guardami wydaje mi się czystsze. Claimsy w pewnym momencie mogłyby narobić trochę burdelu w kodzie, a tymi guardami mogę zrobić wszystko. Być może to trochę na wyrost, ale mechanizm jest stosunkowo prosty i kosztowo porównywalny :)

Swoją drogą jestem ciekawy, jak to się robi w innych aplikacjach. Nie potrafię teraz oszacować ilości requestów na sekundę. Mam nadzieję, że opcja z rzutowaniem na dynamic się sprawdzi. Jeśli nie, będzie trzeba wymyślić coś innego. Może wtedy faktycznie pokombinuję z Claimsami.

0
Juhas napisał(a):

No właśnie nie da się tak, co opisałem kodem powyżej :)

No tak, teraz widzę. Ale to dlatego, że kod, który wkleiłeś teraz jednak znacząco różni się od tego z pierwszego pytania. Faktycznie z property typu object nic się nie zrobi.

No właśnie nie. Dodając nowy kontroler, nie muszę zmieniać nic w Guardach ze względu na tą generyczną wersję metody CheckController. Dlatego też nie wiążę PlanGuarda z każdym kontrolerem, tylko PlanGuard obsługuje tylko te kontrolery, które mają mieć jakieś ograniczenie. Akcje dla konkretnych kontrolerów są sprawdzane w metodzie CheckController w taki sposób:

No tak, więc dodasz nowy kontroler i użytkownik będzie mógł zrobić wszytko. Bo źródłem danych o uprawnieniach użytkownika nie są cechy ani role użytkownika tylko klasa GUI. A cały mechanizm opiera się na hardcodowaniu nazw klas i metod, zamiast na weryfikacji użytkownika. Bezpieczeństwo Twojej aplikacji nie zależy od kto daną akcję faktycznie wykonuje tylko od tego jak ona się nazywa.

Jest to jakieś rozwiązanie, ale moim zdaniem zbyt ubogie. Moje rozwiązanie było czasowo porównywalne kosztowo do Twojego. Natomiast umożliwia mi dużo więcej. Być może za miesiąc jakiś plan w ogóle nie będzie mógł wykorzystać jakiejś akcji albo będzie musiał zareagować w specyficzny sposób.

Możesz podać konkretny przykład czego się nie da na claimsach zrobić, a da się na guardach? Przecież claimsy to tylko metadane, to jak i gdzie je interpretujesz to inna rzecz. Ale dzięki temu możesz dokonywać weryfikacji w różnych miejscach aplikacji, a nie jednym godobject jak masz teraz.

Swoją drogą jestem ciekawy, jak to się robi w innych aplikacjach.

Generalnie tak jak opisałem.

0
somekind napisał(a):

No tak, więc dodasz nowy kontroler i użytkownik będzie mógł zrobić wszytko. Bo źródłem danych o uprawnieniach użytkownika nie są cechy ani role użytkownika tylko klasa GUI. A cały mechanizm opiera się na hardcodowaniu nazw klas i metod, zamiast na weryfikacji użytkownika. Bezpieczeństwo Twojej aplikacji nie zależy od kto daną akcję faktycznie wykonuje tylko od tego jak ona się nazywa.

Nie nie. Łączysz ze sobą dwie rzeczy. Autoryzacja to co innego. Do tego używam polityk. Polityka mi mówi, czy użytkownik może w ogóle daną akcję wykonać (np. dodać klienta). A PlanGuard mi mówi, czy użytkownik może dodać KOLEJNEGO klienta.

Jest to jakieś rozwiązanie, ale moim zdaniem zbyt ubogie. Moje rozwiązanie było czasowo porównywalne kosztowo do Twojego. Natomiast umożliwia mi dużo więcej. Być może za miesiąc jakiś plan w ogóle nie będzie mógł wykorzystać jakiejś akcji albo będzie musiał zareagować w specyficzny sposób.

Możesz podać konkretny przykład czego się nie da na claimsach zrobić, a da się na guardach? Przecież claimsy to tylko metadane, to jak i gdzie je interpretujesz to inna rzecz. Ale dzięki temu możesz dokonywać weryfikacji w różnych miejscach aplikacji, a nie jednym godobject jak masz teraz.

Czyli mówisz, żeby wszystkie ograniczenia dawać w Claimsach? W stylu:
MaxClientsNum: 5
MaxCośtamNum: 4

itd? A jeśli tych ograniczeń mam powiedzmy ze 20 różnych. To one później wędrują w requeście - jak każdy Claim w JWT Bearer. Czy to normalne?

1
Juhas napisał(a):

Nie nie. Łączysz ze sobą dwie rzeczy. Autoryzacja to co innego. Do tego używam polityk. Polityka mi mówi, czy użytkownik może w ogóle daną akcję wykonać (np. dodać klienta). A PlanGuard mi mówi, czy użytkownik może dodać KOLEJNEGO klienta.

Ja rozumiem Twój kod i rozumiem problem. Dlatego pisałem o weryfikacji, nie autoryzacji - bo w jakiś sposób weryfikujesz uprawnienia klienta.

itd? A jeśli tych ograniczeń mam powiedzmy ze 20 różnych. To one później wędrują w requeście - jak każdy Claim w JWT Bearer. Czy to normalne?

Nie, nie! Nic w requeście nie przychodzi.
W metodzie, w której logujesz użytkownika po prostu pobierz te wartości, utwórz z nich swoje customowe claimsy (obiekty klasy Claim) i umieść je w obiekcie zalogowanego użytkownika. Jest na to trochę przykładów:
https://joonasw.net/view/adding-custom-claims-aspnet-core-2
https://stackoverflow.com/questions/48652426/implementing-custom-claim-with-extended-mvc-core-identity-user

1 użytkowników online, w tym zalogowanych: 0, gości: 1