Laden...

ASP.NET Core MVC Magic Strings

Erstellt von WarLorD_XaN vor 2 Jahren Letzter Beitrag vor 2 Jahren 351 Views
W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 2 Jahren
ASP.NET Core MVC Magic Strings

Hallo,

ich versuche gerade in meiner ASP.NET Core Anwendung die ganzen Magic-Strings, welche im Zusammenhang mit Controller und Action Namen auftreten, zu refaktorisieren.

Bisher habe ich das immer so gemacht:


RedirectToAction<ProductsController>(nameof(ProductsController.Details), new { id = 1234 });

Den Controller Namen hole ich mir dabei über eine Helper-Methode:


public static string ControllerName<TController>() => typeof(TController).Name.TrimEnd("Controller");

Ich würde in Zukunft jedoch gerne so etwas in der Art aufrufen können:


RedirectToAction<ProductsController>(pc => pc.Details, new { id = 1234 });

Dabei habe ich aber Probleme dass ich dann für jede mögliche Action-Methoden-Signatur eine Überladung bereitstellen müsste.
Also so:


protected RedirectToActionResult RedirectToAction<TController>(Expression<Func<TController, Func<IActionResult>>> action);
protected RedirectToActionResult RedirectToAction<TController>(Expression<Func<TController, Func<int, IActionResult>>> action);

Ich finde das aber nicht sehr schön.

Ich bin leider nicht so fit mit Expressions, gibt es da eventuell etwas mit dem ich den Namen einer Methode zurückbekommen kann?

Oder gibt es überhaupt für mein Grundprobem (Magic-Strings) eine komplett andere Lösung?

Schöne Grüße,
Xan

16.806 Beiträge seit 2008
vor 2 Jahren

Das kommt drauf an, wie sehr und in welche Richtung Du optimieren willst.

Für das Routing verwendet man in größeren Anwendungen eigentlich Routes. Die sind genau dafür da.
Routes funktionieren aber anhand eines konstenten, eindeutigen Bezeichners, sodass Du dafür entsprechend einfach Konstanten verwenden kannst.

Routes kannst Du dann wiederum für den LinkGenerator verwenden, der für solche Aktionen gedacht ist.
Der LinkGenerator ist auch die performanteste Lösung im Framework für das Routing. Wir - gfoidl und ich - haben für das Forum hier einen eigenen LinkGenerator gebaut, der nochmals xxx-Fach schneller ist als der Standard-LinkGenerator - aber auf Sting-Operationen mit Pattern funktioniert.

Beispiel:


        // Templates
        public const string Users = Root + "users";
        public const string User = Users + "/{userId}";
        public const string UserWithName = User + "/{userNameSlug}";

        // Router
        UrlRelative ToUserProfileView(int userId, string? userName)
        {
            string userNameSlug = GetUserNameSlug(userId, userName);
            return Template(RouteTemplates.UserWithName.NamedFormat(userId, userNameSlug));
        }

Wir haben also ein IPortalRouter, das wir von überall aufrufen und damit sehr performant und sicher navigieren lassen können.
Ja - das ist statisch modular aber nicht dynamisch modular; bewusst so.
Expressions sind für solche Dinge, die sehr oft aufgerufen werden, eine enorme Leistungsbremse - sofern das überhaupt in Deinem Fokus ist.

Dabei habe ich aber Probleme dass ich dann für jede mögliche Action-Methoden-Signatur eine Überladung bereitstellen müsste.

Das liegt daran, dass Du eben nicht die RouteValueDictionary-Schnittstelle dafür verwendest.

2.078 Beiträge seit 2012
vor 2 Jahren

Gib doch in der Expression anstelle eines Func einen Delegate zurück - sollte denke ich funktionieren, ist aber ungetestet.

Ansonsten kannst Du mit einer generischen Klasse arbeiten, also folgende Syntax:


Controller<MyController>.Action((MyController x) => x.MyAction);

Die Action-Methode gibt's dann genauso oft, wie es Func-Delegaten gibt, also generisch mit einer beliebigen Anzahl generischer Parameter und am Ende einem Return.
Die Aufteilung in Klasse und Methoden ist notwendig, da Du, wenn Du einen generischen Typ angibst (den Controller) auch immer alle anderen Typen angeben musst.
Auf diese Weise gibst Du ihn an der Klasse an, bei der Methode kann der Compiler wieder Zucker drüber streuen.
Außerdem hätte das den nützlichen Neben-Effekt, dass Du die Parameter vom Compiler (zumindest die Typen) überprüfen lassen kannst.

Tatsächlich würde ich dein Vorhaben aber als Mikro-Management bezeichnen und es auch so machen:


RedirectToAction<ProductsController>(nameof(ProductsController.Details), new { id = 1234 });

Zumindest bei produktiven Projekten - privat ist so eine Klasse sicher ein nettes kleines Projekt und spart danach auch etwas Zeit ein. Produktiv dürfte die Ersparnis aber vom Aufwand der Entwicklung dieser Klasse sowie späterer Wartung aufgefressen werden.
Außerdem kann Performance ein Problem werden - Expressions sind langsam und groß (viel zu tun für den GC), aber dazu können Abt oder gfoidl sicher noch mehr sagen.

PS @Abt:
Ich glaube, hier geht es wirklich nur um die kleine Code-Optimierung, dass man Controller und Action immer, wie im ersten Snippet gezeigt, angeben muss.
Oder es geht darum, dass der Controller-Name immer Klassen-Name ohne "Controller" am Ende ist, man kann also kein nameof benutzen.
Also eine kleine Schreibarbeit-Ersparnis für etwas, das es so mehr oder weniger schon gibt.
Zumindest habe ich es so verstanden.

16.806 Beiträge seit 2008
vor 2 Jahren

Ich glaube, hier geht es wirklich nur um die kleine Code-Optimierung, dass man Controller und Action immer, wie im ersten Snippet gezeigt, angeben muss.

Jo, bin ich bei Dir, aber er hatte ja auch gefragt, ob es andere Lösungen gibt.

Außerdem kann Performance ein Problem werden - Expressions sind langsam und groß (viel zu tun für den GC), aber dazu können Abt oder gfoidl sicher noch mehr sagen.

Generell zum Routing war das ein eigener Issue bei uns (Bild anbei).
Der eh schon schnellere LinkGenerator hat bei uns 30% performance gekostet - wir konnten damit 390 Requests pro Sekunde als Referenz ausliefern.
Man muss dazu sagen, dass die 30% hier aber viel sind, weil wir andere Bestandteile der App schon enorm optimiert haben.


    checks.....................: 100.00% ✓ 11683 ✗ 0
    data_received..............: 967 MB  32 MB/s
    data_sent..................: 3.0 MB  100 kB/s
    http_req_blocked...........: avg=54.09µs min=0s      med=0s     max=38ms     p(90)=0s      p(95)=0s
    http_req_connecting........: avg=853ns   min=0s      med=0s     max=498.5µs  p(90)=0s      p(95)=0s
    http_req_duration..........: avg=51.24ms min=35ms    med=49.5ms max=213.99ms p(90)=57.99ms p(95)=60.5ms
    http_req_receiving.........: avg=10.62ms min=499.3µs med=10ms   max=77.99ms  p(90)=13.5ms  p(95)=15.5ms
    http_req_sending...........: avg=24.64µs min=0s      med=0s     max=1.49ms   p(90)=0s      p(95)=0s
    http_req_tls_handshaking...: avg=52.21µs min=0s      med=0s     max=36.5ms   p(90)=0s      p(95)=0s
    http_req_waiting...........: avg=40.59ms min=23.5ms  med=39.5ms max=154.5ms  p(90)=46ms    p(95)=49.49ms
    http_reqs..................: 11683   388.798268/s
    iteration_duration.........: avg=51.38ms min=35ms    med=49.5ms max=249.99ms p(90)=57.99ms p(95)=60.99ms
    iterations.................: 11683   388.798268/s
    vus........................: 20      min=20  max=20
    vus_max....................: 20      min=20  max=20

Durch den Umbau auf String-Patterns haben die die Performance auf unter 3,73% Anteil gedrückt und konnten auf der Referenzmaschine 578 Requests pro Sekunde ausliefern.
Also 67% Leistungssteigerung nur durch diesen (kleinen) Umbau.


    checks.....................: 100.00% ✓ 17355 ✗ 0
    data_received..............: 1.4 GB  48 MB/s
    data_sent..................: 4.5 MB  148 kB/s
    http_req_blocked...........: avg=35.14µs  min=0s      med=0s      max=33ms     p(90)=0s       p(95)=0s
    http_req_connecting........: avg=719ns    min=0s      med=0s      max=999.5µs  p(90)=0s       p(95)=0s
    http_req_duration..........: avg=34.46ms  min=16.99ms med=34.5ms  max=101.99ms p(90)=41.99ms  p(95)=44.99ms
    http_req_receiving.........: avg=333.86µs min=0s      med=499.1µs max=53.5ms   p(90)=501.49µs p(95)=502.29µs
    http_req_sending...........: avg=21.72µs  min=0s      med=0s      max=2ms      p(90)=0s       p(95)=0s
    http_req_tls_handshaking...: avg=33.82µs  min=0s      med=0s      max=32ms     p(90)=0s       p(95)=0s
    http_req_waiting...........: avg=34.11ms  min=16.49ms med=34.49ms max=101.99ms p(90)=41.5ms   p(95)=44.49ms
    http_reqs..................: 17355   577.864345/s
    iteration_duration.........: avg=34.58ms  min=16.99ms med=34.51ms max=101.99ms p(90)=42ms     p(95)=45ms
    iterations.................: 17355   577.864345/s
    vus........................: 20      min=20  max=20
    vus_max....................: 20      min=20  max=20

Kann in vielen Szenarien also ein Leistungsblocker sein, der am Ende das Sizing und damit die Kosten beeinflusst.

W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 2 Jahren

Erstmal danke für die ausführlichen Antworten.

Vielleicht kurz zum Hintergrund:
Es ist eine Anwendung zur Abwicklung eines Fahrradbasars.
Die läuft nur in einem lokalen Netzwerk mit maximal 20 Usern, daher würde ich Perfomance nicht so kritisch einstufen.
Grundsätzlich läuft das ganze auch schon und ist stabil.

Ich aber bin gerade dabei die Anwendung von simplen Razor Pages auf MVC zu portieren.
Hauptsächlich weils mir Spaß bereitet und ich will etwas Erfahrung sammeln und was neues Lernen.
Also bin ich auch für komplett andere Ansätze offen.

So nun zum Thema Routing:
Das heißt ich habe Zentral eine Stelle wo ich meine Magic-Strings definiere (z.B. als Konstanten).
In der Anwendung verwende ich dann, wenn ich einen Link benötige (UI, oder in einem Controller) den LinkGenerator.
Wobei ich dann ExtensionMethods für den LinkGenerator habe, welche mir meine spezialitäten abdecken.

In meinen Controllern rufe ich dann nur die simple Redirect(string url) Methode auf.
In meinen Views verwende ich dann anstatt dem AnchorTagHelper direkt <a href="....">.

Habe ich das so richtig verstanden?

Was mir dabei halt nicht so gefällt ist, dass ich dann die ExtensionMethods immer mit meinen Controllern abgleichen muss.

Zum Thema mit dem Delegate:
Das mit dem Delegate anstatt der Func war mein erster Ansatz, das kompiliert mir aber nicht:
CS1662
CS0428

Zum Thema generische Klasse:
Jo das würde so gehen, aber da finde ich die nameof Syntax noch schöner.

Wenn aber Expressions teuer sind, dann kann ich in diesem Fall auch darauf verzichten.
An manchen Stellen kommt man eh nicht drumrum (z.B. in den Views)

Ich denke ich werde mir das mit dem Routing mal näher ansehen, das ist bisher eh noch ein weißer Fleck auf meiner ASP.NET Landkarte 😉

Schöne Grüße,
Xan

16.806 Beiträge seit 2008
vor 2 Jahren

Habe ich das so richtig verstanden?

Wie gesagt; das kommt drauf an, was Du willst.
Technisch funktionieren hier alle genannten Variante.

Vorteile den Router zu abstrahieren, wie wir im Forum gemacht haben, ist - wenn Du den LinkGenerator verwenden willst, halt auch, dass Du die Parameter entsprechend in einer eigenen Methode Vordefinieren kannst und damit Compiler Support zur Entwicklungszeit hast:
Durch die Typsicherheit kannste nie Parameter vergessen oder im falschen Format mitliefern. Wenn Du nameof(ProductsController.Details), new { id = 1234 } schreibst kannst Parameter falsch schreiben, vergessen oder den falschen Typ einsetzen - und es knallt erst zur Laufzeit.

In meinen Controllern rufe ich dann nur die simple Redirect(string url) Methode auf.
In meinen Views verwende ich dann anstatt dem AnchorTagHelper direkt <a href="....">.

Korrekt. Wir im Forum verwenden nich einen Tag-Helper zum Routen (mach ich aber schon Jahre nicht mehr). Das Problem bei den TagHelpern enpfinde ich, dass sie niemals Fehler ausspucken. Die Werte sind einfach leer wenn etwas nicht stimmt.
Und das mit Tests abzuprüfen ist im Gegensatz zu einer Router Abstraktion extrem aufwendig.

Bei uns sieht das einfach so aus:


// View
@model UserProfileViewModel
@inject IPortalRouter Router
string currentUrl = Router.ToUserProfileView(profile.UserId, profile.UserName); // wird dann in mehreren a href= verwendet, sodass auch die Url nur ein mal pro View generiert werden muss.

// Controller
return Redirect(_router.ToUserProfileView(user.UserId, user.UserName));

// Deklaration der Action
[Route(RouteTemplates.UserWithName)]
[FeatureGate(FeatureFlags.UsersEnabled, FeatureFlags.UsersProfileView)]
public async Task<IActionResult> Profile(int userId, string userNameSlug)

Was mir dabei halt nicht so gefällt ist, dass ich dann die ExtensionMethods immer mit meinen Controllern abgleichen muss.

Davon abgesehen, dass ichs eher abstrahieren als erweitern würde (weil es sich so unabhängig implementieren lässt), musst Du halt einen Weg gehen - und keiner ist perfekt; in keinem Framework.
Zu MVC 3 Zeiten gabs mal T4 Templates, die anhand einer XML Struktur Actions und Router erzeugt haben und so alles automatismus-getrieben synchron war - hat sich nie durchgesetzt.

W
WarLorD_XaN Themenstarter:in
113 Beiträge seit 2006
vor 2 Jahren

Alles klar dann werde ich das mal mit dem LinkGenerator versuchen.

Der AnchorTagHelper ist mir sowieso die meiste Zeit eher im Weg, da hab ich einiges drum-rum bauen müssen.

Besten Dank für die Hilfe!